xyk blog

最近は iOS 開発の記事が多めです。

UICollectionView で複数 Section の Header と Footer を表示する

検証環境:
Xcode 12.4
Swift 5.3.2

UICollectionView で複数セクションのヘッダーとフッターを表示する方法について。

f:id:xyk:20210226145730p:plain

StoaryBoard 設定

まず StoryBoard で Root View に UICollectionView 及び UICollectionViewCell のビューを設置しておく。
UICollectionView と UICollectionViewDelegateFlowLayout は @IBOutlet でコード上の定義と紐付ける。

f:id:xyk:20210226121457p:plain ヘッダービュー、フッタービュー(UICollectionReusableView)は StoryBoard 上で設置できるのは各1つのみで複数の設置はできない。(同じビューを使い回すのは可能)
なので複数のヘッダービュー、フッタービューを表示したい場合はコード側でやるしかない(と思われる)。
今回はNibファイル(.xib)で用意し、collectionView#register メソッドで登録する。

f:id:xyk:20210226114633p:plain

ヘッダー、フッターを表示する

ヘッダー、フッターを表示させるには、ビューのサイズを指定する必要がある。
1つは、UICollectionViewFlowLayout のheaderReferenceSizeまたはfooterReferenceSizeプロパティでサイズを指定する方法、もう1つは UICollectionViewDelegate のcollectionView(_:layout:referenceSizeForHeaderInSection:)またはcollectionView(_:layout:referenceSizeForFooterInSection:)のデリゲートメソッドでサイズを指定する方法のどちらか。
このサイズに 0 を設定するとヘッダー、フッターは表示されなくなる。

コード側で指定しなくても StoryBoard 側で UICollectionView にある以下のチェックを入れるとヘッダー、フッターが自動生成されるので、これでも表示させることができる。

f:id:xyk:20210226120006p:plain

この場合は先にも書いたとおり追加できるのはヘッダー、フッター各1つのみ。
今回は複数のヘッダー、フッターを追加するのが目的なのでここは使わない。

サンプルコード

import UIKit

class ViewController: UIViewController {
    
    enum Section: Int, CaseIterable {
        case section1 = 0
        case section2
    }
    
    var items1: [String] = ["1-1", "1-2", "1-3", "1-4", "1-5", "1-6"]
    var items2: [String] = ["2-1", "2-2", "2-3", "2-4", "2-5", "2-6"]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionView.register(
                UINib(nibName: "Section1HeaderView", bundle: nil),
                forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                withReuseIdentifier: "Section1HeaderView")
            collectionView.register(
                UINib(nibName: "Section1FooterView", bundle: nil),
                forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter,
                withReuseIdentifier: "Section1FooterView")
            collectionView.register(
                UINib(nibName: "Section2HeaderView", bundle: nil),
                forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                withReuseIdentifier: "Section2HeaderView")
            collectionView.register(
                UINib(nibName: "Section2FooterView", bundle: nil),
                forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter,
                withReuseIdentifier: "Section2FooterView")
            
            collectionView.dataSource = self
            collectionView.delegate = self
            collectionView.alwaysBounceVertical = true
        }
    }
    
    @IBOutlet weak var flowLayout: UICollectionViewFlowLayout! {
        didSet {
            flowLayout.estimatedItemSize = .zero // Self-Sizing off
            flowLayout.scrollDirection = .vertical
            flowLayout.sectionHeadersPinToVisibleBounds = true
            flowLayout.sectionFootersPinToVisibleBounds = false
        }
    }
}

extension ViewController: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return Section.allCases.count
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        switch Section(rawValue: section)! {
        case .section1:
            return items1.count
        case .section2:
            return items2.count
        }
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
        cell.contentView.backgroundColor = (indexPath.item % 2 == 0) ? .systemGray4 : .systemGray6
        switch Section(rawValue: indexPath.section)! {
        case .section1:
            cell.titleLabel.text = items1[indexPath.item]
        case .section2:
            cell.titleLabel.text = items2[indexPath.item]
        }
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        switch kind {
        case UICollectionView.elementKindSectionHeader:
            switch Section(rawValue: indexPath.section)! {
            case .section1:
                return collectionView.dequeueReusableSupplementaryView(
                    ofKind: kind, withReuseIdentifier: "Section1HeaderView", for: indexPath)
            case .section2:
                return collectionView.dequeueReusableSupplementaryView(
                    ofKind: kind, withReuseIdentifier: "Section2HeaderView", for: indexPath)
            }
        case UICollectionView.elementKindSectionFooter:
            switch Section(rawValue: indexPath.section)! {
            case .section1:
                return collectionView.dequeueReusableSupplementaryView(
                    ofKind: kind, withReuseIdentifier: "Section1FooterView", for: indexPath)
            case .section2:
                return collectionView.dequeueReusableSupplementaryView(
                    ofKind: kind, withReuseIdentifier: "Section2FooterView", for: indexPath)
            }
        default:
            fatalError()
        }
    }
}

extension ViewController: UICollectionViewDelegate {
    
}

extension ViewController: UICollectionViewDelegateFlowLayout {
    
    // セルのサイズ
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 90, height: 90)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 10
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 10
    }
    
    // ヘッダーのサイズ
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        switch Section(rawValue: section)! {
        case .section1:
            return CGSize(width: collectionView.frame.width, height: 60)
        case .section2:
            return CGSize(width: collectionView.frame.width, height: 60)
        }
    }
    
    // フッターのサイズ
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
        switch Section(rawValue: section)! {
        case .section1:
            return CGSize(width: collectionView.frame.width, height: 30)
        case .section2:
            return CGSize(width: collectionView.frame.width, height: 30)
        }
    }
}

class CollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var titleLabel: UILabel!
}

class Section1HeaderView: UICollectionReusableView {
    
}

class Section1FooterView: UICollectionReusableView {
    
}

class Section2HeaderView: UICollectionReusableView {
    
}

class Section2FooterView: UICollectionReusableView {
    
}

メニューバーに常駐する masOS アプリを作る

検証環境:
Xcode 12.4
Swift 5.3.2

メニューバーに常駐する簡単な masOS アプリを作ってみる。

f:id:xyk:20210223123827p:plain

  • まず Xcode のプロジェクト作成から macOS -> App テンプレートを選択する。
    AppDelegate.swift ファイルに以下コードを書く。
    NSMenu と NSMenuItem でメニューを作成、それを NSStatusItem にセットする。
import Cocoa

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    
    // NSStatusItem.variableLength でメニューバーの横幅が可変となる。 variableLength: -1.0, squareLength: -2.0
    let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let menu = NSMenu()
        menu.addItem(NSMenuItem(title: "menu1", action: #selector(menu1), keyEquivalent: ""))
        menu.addItem(NSMenuItem(title: "menu2", action: #selector(menu2), keyEquivalent: ""))
        menu.addItem(NSMenuItem.separator())
        menu.addItem(NSMenuItem(title: "quit", action: #selector(quit), keyEquivalent: "q"))
        statusItem.menu = menu
        
        statusItem.button?.title = "Sample"
    }
    
    @objc func menu1(_ sender: NSMenuItem) {
        print("click menu1")
    }
    
    @objc func menu2(_ sender: NSMenuItem) {
        print("click menu2")
    }
    
    @objc func quit(_ sender: NSMenuItem) {
        print("quit")
        NSApplication.shared.terminate(self)
    }
}
  • Main.storyboard を開き、WindowController を選択し、Is Initial Controllerのチェックを外しておく。
    これで空のウインドウが表示されなくなる。

  • info.plist を開き、プラスボタンで1行追加する。そして
    Key にApplication is agent (UIElement)を選択し、ValueYESとする。
    Source Code として開くと以下の xml が追加されている。

<key>LSUIElement</key>
<true/>

これで Dock 内にこのアプリケーションが表示されなくなる。

macOS でのカーソル移動の高速化

macOS でのカーソル移動の高速化をシステム環境設定からではなくdefaultsコマンドで変更する方法について。

f:id:xyk:20210223113805p:plain

システム環境設定からキーのリピートリピート入力認識までの時間ともに最速に設定しているが、defaultsコマンドを使うことでさらに高速化できる。

変更前に現在の設定値を確認する。

$ defaults read -g KeyRepeat
2
$ defaults read -g InitialKeyRepeat
15

最速に変更する。

$ defaults write -g KeyRepeat -int 1
$ defaults write -g InitialKeyRepeat -int 10

UICollectionView でタグクラウド風のレイアウトを実現する

検証環境:
Xcode 12.4
Swift 5.3.2

UICollectionView を使ってタグクラウド風にセルが並ぶレイアウトを実現したい。
UICollectionView のデフォルトのレイアウトである UICollectionViewFlowLayout をそのまま使うと以下のようにセル間にスペースが入ってしまう。

f:id:xyk:20210217001614p:plain

これをスペースを開けずに左寄せに配置されるカスタムレイアウトを作成する。

f:id:xyk:20210217001631p:plain

コードサンプル

StoryBoard 側で UICollectionView を配置して collectionView と flowLayout を IBOutlet で接続しておく。

import UIKit

class ViewController: UIViewController {
    
    var items: [String] = [
        "ビアガーデン",
        "うなぎ",
        "韓国料理",
        "焼肉",
        "焼き鳥",
        "ステーキ",
        "そば",
        "バイキング・ビュッフェ",
        "しゃぶしゃぶ",
        "鉄板焼き",
        "ピザ",
        "食堂・定食",
        "パン・サンドイッチ",
        "カフェ",
        "弁当",
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()   
    }
    
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionView.dataSource = self
            collectionView.delegate = self
            collectionView.alwaysBounceVertical = true
            collectionView.allowsMultipleSelection = false
        }
    }
    
    @IBOutlet weak var flowLayout: UICollectionViewFlowLayout! {
        didSet {
            flowLayout.minimumLineSpacing = 8
            flowLayout.minimumInteritemSpacing = 8
            flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
            flowLayout.scrollDirection = .vertical
            flowLayout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
        }
    }
}

extension ViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
        cell.textLabel.text = items[indexPath.item]
        return cell
    }
}

extension ViewController: UICollectionViewDelegate {
    
}

extension ViewController: UICollectionViewDelegateFlowLayout {
    
}

class CollectionViewCell: UICollectionViewCell {
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        textLabel.textColor = UIColor.systemBlue
        textLabel.font = .systemFont(ofSize: 14)
        
        layer.backgroundColor = UIColor.white.cgColor
        layer.borderWidth = 2
        layer.borderColor = UIColor.systemBlue.cgColor
        layer.cornerRadius = 16
        layer.masksToBounds = true
        
        // iOS12のみ以下制約をつけないとAutoLayoutが効かない
        contentView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            contentView.leftAnchor.constraint(equalTo: leftAnchor),
            contentView.rightAnchor.constraint(equalTo: rightAnchor),
            contentView.topAnchor.constraint(equalTo: topAnchor),
            contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
    
    // セル選択で色を反転させる
    override var isSelected: Bool {
        didSet {
            if isSelected {
                backgroundColor = UIColor.systemBlue
                textLabel.textColor = .white
            } else {
                backgroundColor = .white
                textLabel.textColor = UIColor.systemBlue
            }
        }
    }
    
    @IBOutlet weak var textLabel: UILabel!
}

// セルを左寄せにするカスタムレイアウトクラス
class CollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
        var currentRowY: CGFloat = -1.0
        var currentRowX: CGFloat = 0
        for attribute in attributes where attribute.representedElementCategory == .cell {
            if currentRowY != attribute.frame.origin.y {
                currentRowY = attribute.frame.origin.y
                currentRowX = sectionInset.left
            }
            attribute.frame.origin.x = currentRowX
            currentRowX += attribute.frame.width + minimumInteritemSpacing
        }
        return attributes
    }
}

セルのサイズ

今回はセル内に1行指定の UILabel があり、テキストの長さによってセルの横幅は可変長となる。

セルのサイズは固定値を指定するのではなく、AutoLayout の制約に従って動的に計算される Self Sizing 機能を利用する。
セルの Self Sizing 機能を有効にするには、UICollectionViewFlowLayout.estimatedItemSizeプロパティを 0 以外にすればよい。
estimatedItemSizeプロパティにはデフォルトでUICollectionViewFlowLayout.automaticSizeが設定されているので既に有効になっている。
なのでUICollectionViewFlowLayout.itemSizeプロパティやcollectionView(layout:sizeForItemAt:)デリゲートメソッドによるサイズ指定はしないようにする。

StoryBoard で UICollectionViewCell に UILabel を配置し、ContentView の四辺のエッジに合うように制約を追加する。

f:id:xyk:20210217003629p:plain

レイアウトクラス

UICollectionViewFlowLayout を継承したサブクラスCollectionViewLeftAlignedLayoutを作成する。
そしてlayoutAttributesForElements(in:)メソッドをオーバーライドし、各セルのframe.origin.xを上書きして左寄せになるように調整する。

f:id:xyk:20210217114146p:plain

UICollectionView のセルサイズを指定しても反映されない時

検証環境:
Xcode 12.4
Swift 5.3.2

UICollectionView のセルサイズを、

  • UICollectionViewFlowLayout.itemSizeプロパティ
  • またはUICollectionViewDelegateFlowLayoutデリゲートのcollectionView(_:layout:sizeForItemAt:)メソッド

を実装してセルサイズ(CGSize)を指定しても、contentView に張り付けた UIImageView の制約の方が優先されて指定したセルサイズにならない現象が起きた。

原因はどうやら UICollectionViewCell の Self Sizing 機能が有効になり、セル内の AutoLayout の制約に従って自動計算されたセルサイズが優先されるらしい。

これを回避する方法はセルの推定サイズestimatedItemSizeプロパティにデフォルトでUICollectionViewFlowLayout.automaticSizeが指定されているのでこれを 0 に変更すればよい。

コードでやる場合

let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
flowLayout.estimatedItemSize = .zero

または

@IBOutlet weak var flowLayout: UICollectionViewFlowLayout! {
    didSet {
        flowLayout.estimatedItemSize = .zero
    }
}

StoryBoard でやる場合

f:id:xyk:20210216104602p:plain

UICollectionViewCell の横幅を計算する

検証環境:
Xcode 12.4
Swift 5.3.2

UICollectionView のセルの横幅をいい感じに調整する方法について。

前提条件として、セルの並び方向はデフォルトのflowLayout.scrollDirection = .vertical、セルのサイズは正方形ですべてのセルが同じサイズであること。

セルの横幅に固定値を指定した場合、セル間の間隔はシステム側で自動に調整してくれるが、セル間の間隔が広くなってしまうと見た目がよくない。
また端末によって画面サイズが違うので1行に配置されるセル数や間隔も変わってくる。

固定値でなく例えば1行にセルが3列入るように横幅を計算する方法もあるが、その場合 iPhone では問題ないが iPad では1行3列だとセルがでかすぎで、せっかくの大画面が生かされない。

実現したいことは大体のセルの最小横幅を指定すると、セル間の隙間は広げず、セル幅を広げて画面に詰まった状態でセルが並ぶように自動調整してくれる機能。

以下サンプルコード。
Constantsのパラメータで調整できる。
StoryBoard も使っているがここでは省略。

import UIKit

class ViewController: UIViewController {
    
    var items: [String] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        for i in 0..<20 {
            items.append("\(i)")
        }
        
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.alwaysBounceVertical = true

        let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
        flowLayout.estimatedItemSize = .zero
    }
    
    @IBOutlet weak var collectionView: UICollectionView!
    
    private struct Constants {
        static let minimumCellWidth: CGFloat = 100 // セルの最小横幅
        static let interItemSpacing: CGFloat = 5 // セル間のマージン
        static let leftRightInset: CGFloat = 5 // 左右の各マージン
        static let lineSpacing: CGFloat = 5 // 行間のマージン
    }
    
    var cachedCellSize: CGSize?
    
    private func calculateCellWidth() -> CGFloat {
        // 左右のInsetを除いた横幅
        let widthWithoutMargin = collectionView.frame.width - (Constants.leftRightInset * 2)
        // 1行のセル数
        let numPerRow = (widthWithoutMargin / Constants.minimumCellWidth).rounded(.down)
        // セルに割り当てる余り幅の算出
        var calculator: ((CGFloat) -> CGFloat)!
        calculator = { (num: CGFloat) -> CGFloat in
            let remainingWidth = widthWithoutMargin - (Constants.minimumCellWidth * num) - (Constants.interItemSpacing * (num - 1))
            if remainingWidth < 0 {
                return calculator(num - 1)
            } else {
                return (remainingWidth / num).rounded(.down)
            }
        }
        let additionalWidth = calculator(numPerRow)
        // セル幅
        let calculatedCellWidth = Constants.minimumCellWidth + additionalWidth
        
        print("collectionViewWidth: \(collectionView.frame.width), widthWithoutMargin: \(widthWithoutMargin), numPerRow: \(numPerRow), additionalWidth: \(additionalWidth), calculatedCellWidth: \(calculatedCellWidth)")
        
        return calculatedCellWidth
    }
}

extension ViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath)
        cell.contentView.backgroundColor = (indexPath.item % 2 == 0) ? .systemTeal : .systemPurple
        return cell
    }
}

extension ViewController: UICollectionViewDelegate {
    
}

extension ViewController: UICollectionViewDelegateFlowLayout {
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        // 今回はすべてのセルが同じサイズという仕様なので初回のみ計算し、それ以降はキャッシュしたサイズを返す
        if let cachedCellSize = cachedCellSize {
            return cachedCellSize
        } else {
            let width = calculateCellWidth()
            cachedCellSize = CGSize(width: width, height: width)
            return cachedCellSize!
        }
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return .init(top: 0, left: Constants.leftRightInset, bottom: 0, right: Constants.leftRightInset)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return Constants.lineSpacing
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return Constants.interItemSpacing
    }
}

class CollectionViewCell: UICollectionViewCell {
}

例えばセルの最小横幅を100ptを指定した場合の各端末の表示

iPod touch 7th generation(4インチ)

f:id:xyk:20210215142610p:plain

collectionViewWidth: 320.0, additionalWidth: 0.0, calculatedCellWidth: 100.0

iPhone SE 2nd gereration(4.7インチ)

f:id:xyk:20210215143710p:plain

collectionViewWidth: 375.0, additionalWidth: 18.0, calculatedCellWidth: 118.0

iPhone 12 Pro Max(6.7インチ)

f:id:xyk:20210215143146p:plain

collectionViewWidth: 428.0, additionalWidth: 0.0, calculatedCellWidth: 100.0

iPad Pro 4th gereration (12.9インチ)

f:id:xyk:20210215144111p:plain

collectionViewWidth: 1024.0, additionalWidth: 8.0, calculatedCellWidth: 108.0

macOS ショートカットメモ

環境: macOS Catalina 10.15.7

自分用の macOS ショートカットメモ。

Dock の表示・非表示

  • command + option + d
    or
  • control + F3

ちなみに表示・非表示アニメーションの速度はdefaultsコマンドで変更できる。

$ defaults write com.apple.dock autohide-time-modifier -float 0.2; killall Dock

完全にアニメーションをオフにするには 0 を指定すればよい。

$ defaults write com.apple.dock autohide-time-modifier -float 0; killall Dock

デフォルトに戻すには以下を実行する。

$ defaults delete com.apple.dock autohide-time-modifier; killall Dock

システム環境設定を開く

  • 直接開くショートカットは存在しない。
    代わりに、Spotlight検索からSystem Preferences.appで呼び出している。
  • システム環境設定を開いている場合、command + l を押すとシステム環境設定のトップ画面に戻る。

システム環境設定のサウンドを開く

  • option + F10 or F11 or F12

ウインドウ最小化するショートカットcommand + m を無効化する

  • ウインドウを最小化するショートカットをcmd + n,やcmd + ,の誤操作でよく発動してしまうので無効化したい。
  • 逆に表示させるショートカットは存在しない。
  • 完全にオフにする方法はないようなので、cmd + mではない別のショートカットキーを割り当てることで発動しないようにする。
設定方法
  • システム環境設定 > キーボード > ショートカット > アプリケーションの「+」ボタンを押す。
    メニュータイトルに「最小化」、キーボードショートカットにcontrol + command + m を設定する。
  • さらに「Minimize」、「しまう」というメニュータイトルでもcontrol + command + m を設定しておく。

f:id:xyk:20210208151951p:plain

f:id:xyk:20210208152004p:plain

新生銀行のスマホ認証サービスの更新方法(iPhone)

新しい iPhone に移行する際に一番面倒だった新生パワーダイレクトのスマホ認証サービスの更新についてのメモ。

新生銀行の新生パワーダイレクトでは、振り込みなどの取引時には Symantec の「VIP Access」アプリを使った認証を行う必要がある。

www.shinseibank.com

この「VIP Access」アプリでは、インストールして起動するとクレデンシャルID(と30秒毎に更新されるセキュリティコード)が発行されるので、それを新生パワーダイレクトに登録して紐付ける必要がある。

クレデンシャルIDは端末固有のものであり、iPhone の機種変更をした際に、古い端末からデータ移行してリストアしても、「VIP Access」アプリのクレデンシャルIDは引き継がれず新しいIDが発行されるので、新生パワーダイレクトにも新しいIDを登録し直す必要がある。
(おそらくID情報は Keychain にkSecAttrAccessible~ThisDeviceOnlyにして保存し、その端末以外では復元できないようにしている)

この新生パワーダイレクトに登録したクレデンシャルIDの解除は自分ではできず、面倒なことにチャットまたは電話による有人対応でないとできない。

f:id:xyk:20210107104845p:plain

今回自分は電話でやったのでその時の手順。

電話の流れ

  • 0120-456-858 に電話
  • 自動音声に従って番号入力、2 -> *1 -> 2 -> 1
  • 店舗番号、口座番号入力
  • 暗証番号4桁入力
  • 生年月日4桁入力
  • オペレーターに繋がるのでスマホ認証サービスの解除をしてもらう
  • クレデンシャルIDの解除完了

Webの流れ

  • 新生パワーダイレクトにログインする
  • 登録情報 > スマホ認証端末の登録・解除
  • セキュリティカードの指定された位置の3つの数字を入力
  • 新しい iPhone で「VIP Access」アプリを起動し、クレデンシャルIDとセキュリティコードをメモ(長押しでコピーできる)
  • アプリで発行されたクレデンシャルIDとセキュリティコードをWeb側に登録
  • クレデンシャルIDの登録完了

たぶんチャットでやったほうが楽だと思うので次回はチャットでやりたい。