SwiftUIのライフサイクル

@main
struct SomeApp: App {
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }.onChange(of: scenePhase) { (phase) in
            switch phase {
            case .active:
                // The scene is in the foreground and interactive.
            case .background:
                // The scene isn’t currently visible in the UI.
            case .inactive:
                // The scene is in the foreground but should pause its work.
            @unknown default:
                fatalError()
            }
        }
    }
}

RxSwiftの簡単運用

f:id:machinemxy:20201018152059p:plain 最近シンプル卓球スコアボードにRxSwiftを導入しました。点数パネルをタッチするとModel更新の同時、Viewも連動で更新するようになりました。

Model:

import Foundation
import RxCocoa
import RxSwift

class Scoreboard {
    var gameL = BehaviorRelay(value: 0)
    var gameR = BehaviorRelay(value: 0)
    var scoreL = BehaviorRelay(value: 0)
    var scoreR = BehaviorRelay(value: 0)
    var serveL = BehaviorRelay(value: 2)
    var serveR = BehaviorRelay(value: 0)
    var serveFirst = Side.left
}

その変化を監視したいプロパティーはBehaviorRelay<元のタイプ>にカプセル化が必要。

ViewController:

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    private var sb = Scoreboard()
    private let disposeBag = DisposeBag()
    @IBOutlet weak var scoreLeft: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        sb.scoreL
            .asObservable()
            .subscribe(onNext: { [unowned self] value in
                scoreLeft.setTitle("\(value)", for: .normal)
            })
            .disposed(by: disposeBag)
    }

    @IBAction func addScoreLeft(_ sender: Any) {
        sb.scoreL.accept(sb.scoreL.value + 1)
    }
}

disposeBagはこのViewControllerは消えた時、全ての監視関係を解除するためのもの。
viewDidLoadに、ModelのscoreLが変化する時、ボタンscoreLeftのTitleをscoreLの値に更新するという関係を構築する。
addScoreLeftの中、ユーザは点数パネルをタッチする時、scoreLの値+1だけ書けばいい。ボタンscoreLeftはsocreLの値変化を感知して、自動的に更新する。

全体のプロジェクトはGitHubで公開しています。

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)
}

Format String

let priceString = "$1"
let formattedString = String(format: "Purchase colorful dice by %@", priceString)
print(formattedString)
>> Purchase colorful dice by $1

formatの中のPlaceholderについて、%@はString型の引数、%dはInt型の引数、%fはDouble型の引数。