iPadでActionSheetを表示する

f:id:machinemxy:20200825233226p:plain iPadでActionSheetを正しく呼び出す元の近くで表示させるため、popoverPresentationControllerのsourceViewとsourceRectの設定が必要です。
例:

@IBAction func dataPressed(_ sender: UIButton) {
    let ac = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
    ac.addAction(UIAlertAction(title: "Save", style: .default, handler: nil))
    ac.addAction(UIAlertAction(title: "Load", style: .default, handler: nil))
    ac.addAction(UIAlertAction(title: "Restart From Beginning", style: .destructive, handler: nil))
    ac.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
    ac.popoverPresentationController?.sourceView = sender
    ac.popoverPresentationController?.sourceRect = sender.bounds
    present(ac, animated: true)
}

MacOS 11 beta: ZStack inside GeometryReader

私のアプリ美しいサイコロMacOS 11 betaでテストする時バグが出た。 本来サイコロは画面の真ん中表示するはずだが、なぜかMacOS 11 betaには左上になってしまいました。

f:id:machinemxy:20200710161835p:plain
本来あるべき姿
f:id:machinemxy:20200710161731p:plain
MacOS 11 betaのバグ
よく調べると、GeometryReaderの中のZStack、本来はGeometryReaderの全てのスペースを占領するという仕様になっているが、なぜか新しいバージョンのSwiftUIにZStackはコンテンツ表示するため最小限のスペースした取らない仕様になってしまいました。

問題は分かると、私はいろいろ試して、ついに解決策を見つけました。 元々のソースコード

GeometryReader { geo in
    ZStack {
        // サイコロの配置
    }
}

修正後のソースコード

GeometryReader { geo in
    ZStack {
        // サイコロの配置
    }.frame(width: geo.size.width, height: geo.size.height)
}

Catalystアプリにタッチバーボタンの実装

例:私のアプリプロ卓球スコアボードに二つのタッチバーボタンで両サイドの点数を追加機能がある。以下は関連のコード。
タッチバーのIdentifierの定義:

#if targetEnvironment(macCatalyst)
import UIKit

extension NSTouchBarItem.Identifier {
    static let addLeft = Self.init("addLeft")
    static let addRight = Self.init("addRight")
}
#endif

ViewControllerにタッチバーの定義:

class GameViewController: UIViewController {
    ...
    #if targetEnvironment(macCatalyst)
    override func makeTouchBar() -> NSTouchBar? {
        let touchBar = NSTouchBar()
        touchBar.delegate = self
        touchBar.defaultItemIdentifiers = [.addLeft, .addRight]
        return touchBar
    }
    #endif
}

タッチバー各ボタンの定義:

#if targetEnvironment(macCatalyst)
extension GameViewController: NSTouchBarDelegate {
    func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
        switch identifier {
        case .addLeft:
            let item = NSButtonTouchBarItem(identifier: identifier, title: "Left+1", target: self, action: #selector(addLeft))
            return item
        case .addRight:
            let item = NSButtonTouchBarItem(identifier: identifier, title: "Right+1", target: self, action: #selector(addRight))
            return item
        default:
            return nil
        }
    }
    
    @objc private func addLeft() {
        ...
    }
    
    @objc private func addRight() {
        ...
    }
}
#endif

新世代のUITableViewController:DiffableDataSource + 検索機能

最終効果:
f:id:machinemxy:20200702204702p:plain
検索欄で検索文字を入力する時表示内容は動的に変更する。

Memo

struct Memo: Hashable {
    var id = UUID().uuidString
    var title = ""
    var detail = ""
    var editTime = Date()
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

MemoListTableViewController

class MemoListTableViewController: UITableViewController {
    enum Section {
        case main
    }
    
    let cellReuseIdentifier = "memo"
    
    var list = [Memo]()
    var dataSource: UITableViewDiffableDataSource<Section, Memo>!

    override func viewDidLoad() {
        super.viewDidLoad()

        // debug data
        for i in 1...10 {
            var memo = Memo()
            memo.title = "Memo\(i)"
            memo.detail = "南无阿弥陀佛南无阿弥陀佛南无阿弥陀佛南无阿弥陀佛南无阿弥陀佛南无阿弥陀佛南无阿弥陀佛南无阿弥陀佛南无阿弥陀佛"
            list.append(memo)
        }
        
        // configure data source
        dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { [unowned self] (tableView, indexPath, memo) -> UITableViewCell? in
            let cell = tableView.dequeueReusableCell(withIdentifier: self.cellReuseIdentifier, for: indexPath)
            cell.textLabel?.text = memo.title
            
            let formatter = DateFormatter()
            formatter.dateStyle = .short
            cell.detailTextLabel?.text = formatter.string(from: memo.editTime) + " " + memo.detail
            
            return cell
        })
        
        update(with: list)
        
        // configure search controller
        let searchController = UISearchController()
        searchController.searchResultsUpdater = self
        searchController.obscuresBackgroundDuringPresentation = false
        navigationItem.searchController = searchController
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let memo = dataSource.itemIdentifier(for: indexPath) else { return }
        
        let detailVC = storyboard?.instantiateViewController(withIdentifier: "detail") as! MemoDetailViewController
        detailVC.memo = memo
        navigationController?.pushViewController(detailVC, animated: true)
    }
    
    private func update(with list: [Memo]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Memo>()
        snapshot.appendSections([.main])
        snapshot.appendItems(list, toSection: .main)
        dataSource.apply(snapshot)
    }
}

extension MemoListTableViewController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        guard let filter = searchController.searchBar.text, !filter.isEmpty else {
            update(with: list)
            return
        }
        
        let lowerCasedFilter = filter.lowercased()
        let filteredList = list.filter { (memo) -> Bool in
            memo.title.lowercased().contains(lowerCasedFilter) || memo.detail.contains(lowerCasedFilter)
        }
        
        update(with: filteredList)
    }
}

アプリのコンテンツ(テキスト、画像など)をシェアする

override func viewDidLoad() {
    super.viewDidLoad()
    ...
    navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(shareTapped))
}

@objc func shareTapped() {
    let avc = UIActivityViewController(activityItems: [someText, someImage], applicationActivities: []
    avc.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem
    present(avc, animated: true)
}

その中、avc.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItemを追加しないと、iPadでボタンを押すとクラッシュすることになる。
画像をシェアする時、info.plistに「Privacy - Photo Library Additions Usage Description」を追加する必要がある。