- 投稿日:2020-09-06T23:39:01+09:00
ファイルを保存するだけで動的にプログラムを更新できるInjectionIIIについて、macOSでもできるようにしてみた
以前、ファイルを保存するだけで動的にプログラムを更新できるInjectionIIIについての解説記事を投稿した。
その際は、対象がiOSだけだった。macOSでもできると書いてあったが、方法がわからず、ようやく少し進展した(まだ道半ば)。
https://qiita.com/KoichiroEto/items/5cb149a6e5d74bbdd66c3. macOSのプログラムからインジェクションしてみる
これまでは、iOS用アプリをインジェクションしていた。iOSアプリはシミュレーター上で動作しており、macOSアプリはそうではないという違いがある。そのため、いくつか追加の手順が必要となる。
3.1. なにかアプリを作る
まず、さきほどと同様に、なにかシンプルなアプリを開発する。
Xcodeを起動→Create a new Xcode Project→macOS→「App」→Next→Product Name:「MacTest」、User Interface: Storyboard→Next→「~/dev」を指定→Create
ViewController.swiftに、以下のようにshow()を追加。viewDidLoad()から呼ばれるようにする。ViewController.swiftclass ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() show() } func show() { let label = NSTextField(frame: NSRect(x: Int.random(in: 10..<100), y: Int.random(in: 10..<100), width: 150, height: 50)) label.backgroundColor = NSColor.cyan label.stringValue = "Hello, world!" view.addSubview(label) } }(TextFieldの位置がランダムなのは、諸事情がある。後述する。)
まずはこの段階で実行してみる。Cmd-R→ビルドされ、実行される。ウィンドウが表示され、「Hello, world!」が表示される。
ViewController.swiftに戻り、"Hello, world!"を"Hello, Japan!"に修正してCmd-Sで保存する。当然、何も反映されない。この時点ではまだインジェクションされていないからだ。
再度Cmd-Rすると、一旦アプリが終了し、再度立ち上げられ、"Hello, Japan!"が表示される。約3秒で立ち上がる。この速度ならあまり不満は持たれないかもしれない。
該当個所を、「Hello, world!」に戻しておく。インジェクションの設定を始めてみよう。3.2. プロジェクトを設定する
Xcodeに戻る。
Cmd-1→MacTestのプロジェクトを選択→PROJECT: MacTest→Build Settings→Linking→Other Linker Flags→ここにカーソルを乗せると左に三角が表示されるので、それをクリックする→Debugの右の「+」をおす→Any Architecture | Any SDK:「-Xlinker -interposable」→リターンを押すと確定する
Cmd-1→MacTestのプロジェクトを選択→TARGETS: MacTest→Signing & Capabilities→All→App Sandoboxの右の小さな「×」を押して、消す
Cmd-1→MacTestのプロジェクトを選択→TARGETS: MacTest→Signing & Capabilities→All→Hardened Runtime→「Disable Library Validation」をcheck3.3. Bundleを追加
AppDelegate.swiftにBundleを追加する。
AppDelegate.swift#if DEBUG Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load() #endif参考までに、AppDelegate.swiftの該当するメソッド全体を示す。
AppDelegate.swiftfunc applicationDidFinishLaunching(_ aNotification: Notification) { #if DEBUG Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load() #endif }3.4. injected()を追加
ViewController.swiftに、injectedというメソッドを追加する。ViewController.swift@objc func injected() { show() }参考までに、ViewController classの全体である。
ViewController.swiftimport Cocoa class ViewController: NSViewController { @objc func injected() { show() } override func viewDidLoad() { super.viewDidLoad() show() NotificationCenter.default.addObserver(self, selector: Selector("injected"), name: NSNotification.Name(rawValue: "INJECTION_BUNDLE_NOTIFICATION"), object: nil) } func show() { let label = NSTextField(frame: NSRect(x: Int.random(in: 10..<100), y: Int.random(in: 10..<100), width: 150, height: 50)) label.stringValue = "Hello, world!" label.backgroundColor = NSColor.cyan view.addSubview(label) } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } }3.5. InjectionIIIにProjectを指定する
InjectionIIIが起動されていなかったら、起動する。
Status menuのInjectionIIIから「Open project」を選択→「~/dev/MacTest」を選択→「Select Project Directory」3.6. 起動する
Cmd-R→アプリが起動して、「Hello, world!」が表示される。
この状態で、Hello, world!を編集してみる。Cmd-Sで保存する。そうすると、即座にコンパイルされ、読み込まれ、classがreplaceされる。
また、injectedが呼ばれ、そこからshowが呼ばれる。
ただ、前のオブジェクトが残ってしまっている。そのため、以前のTextFieldは消去されない。そのまま残るだけである。
そのため、以前のversionでは場所が固定されているので、文字が変更されない。これが更新される方法は、これから調べる予定。とりあえず、今日はここまで!
- 投稿日:2020-09-06T23:05:36+09:00
App Store Connect でアプロードした時 Too many symbol files のかべにぶち当たったときの対処方法
- dSYM ファイルがいっぱいあることが原因っぽい (この辺は曖昧)
- 調べると dSYM ファイルをどうにかするっていう対処法ばっかりだったからそう思ってる
調べた感じ2つ
1つ目
プロジェクトファイルの debug information format を
DWARFにする
dSYMファイルを生成しないという設定らしい (多分)
でも Firebase Crashlytics で dSYM ファイルを使用してどうのこうのするっぽいから生成しないのはちょっとなと思う2つ目
Podfile に
config.build_settings['VALID_ARCHS'] = 'arm64'を追加するこれで余分な architecture のdSYMファイルを生成しないでよいっぽい
アーカイブしたファイル(.xcarchive)のパッケージの内容を表示して、dSYMsのフォルダ以下で dwarfdump --uuid * を実行すると下記のように、ライブラリごとに対応しているarchitecture 一覧が見れる。
下記はオプションを入れる前の状態
dSYMs % dwarfdump --uuid * UUID: 081C9609-5C30-3CB0-84B7-DF3326EF8146 (arm64) Kingfisher.framework.dSYM/Contents/Resources/DWARF/Kingfisher UUID: 24163A65-4498-37DB-BEBF-F6227DBFF77C (arm64) Realm.framework.dSYM/Contents/Resources/DWARF/Realm UUID: 4165D0CD-F99B-36EB-BC2F-8661D5056F33 (arm64) RealmSwift.framework.dSYM/Contents/Resources/DWARF/RealmSwift UUID: 70DDE517-8A61-3CE3-B1F4-E4B23FBBAD38 (armv7) Rswift.framework.dSYM/Contents/Resources/DWARF/Rswift UUID: B1138461-D58C-34A9-805C-CAB35DFC9141 (arm64) Rswift.framework.dSYM/Contents/Resources/DWARF/Rswift UUID: 8A08736F-4BF9-3F50-8A33-B1F2A4A44AAA (arm64) RxCocoa.framework.dSYM/Contents/Resources/DWARF/RxCocoa UUID: 0EBDB9F9-30C9-3E3C-9C84-B8FFAFF152FE (arm64) RxRelay.framework.dSYM/Contents/Resources/DWARF/RxRelay UUID: 8EC6ED8C-9989-32F8-88EF-8D4FED75868B (arm64) RxSwift.framework.dSYM/Contents/Resources/DWARF/RxSwift UUID: 5D9C7297-AE8C-362F-AB92-72926B9243A2 (armv7) SwiftyBeaver.framework.dSYM/Contents/Resources/DWARF/SwiftyBeaver UUID: DA55A53C-0AB8-35D6-B00B-3BF11D0A060A (arm64) SwiftyBeaver.framework.dSYM/Contents/Resources/DWARF/SwiftyBeaver下記がオプションを入れた状態
UUID: 081C9609-5C30-3CB0-84B7-DF3326EF8146 (arm64) Kingfisher.framework.dSYM/Contents/Resources/DWARF/Kingfisher UUID: 24163A65-4498-37DB-BEBF-F6227DBFF77C (arm64) Realm.framework.dSYM/Contents/Resources/DWARF/Realm UUID: 4165D0CD-F99B-36EB-BC2F-8661D5056F33 (arm64) RealmSwift.framework.dSYM/Contents/Resources/DWARF/RealmSwift UUID: B1138461-D58C-34A9-805C-CAB35DFC9141 (arm64) Rswift.framework.dSYM/Contents/Resources/DWARF/Rswift UUID: 8A08736F-4BF9-3F50-8A33-B1F2A4A44AAA (arm64) RxCocoa.framework.dSYM/Contents/Resources/DWARF/RxCocoa UUID: 0EBDB9F9-30C9-3E3C-9C84-B8FFAFF152FE (arm64) RxRelay.framework.dSYM/Contents/Resources/DWARF/RxRelay UUID: 8EC6ED8C-9989-32F8-88EF-8D4FED75868B (arm64) RxSwift.framework.dSYM/Contents/Resources/DWARF/RxSwift UUID: DA55A53C-0AB8-35D6-B00B-3BF11D0A060A (arm64) SwiftyBeaver.framework.dSYM/Contents/Resources/DWARF/SwiftyBeaveramrv7(iPhone5, iPhone5c以下の端末)はメインプロジェクトで使用しないので、不要となる。
これでアップロードすればきっとだいじょうぶなはず。
- 投稿日:2020-09-06T22:21:53+09:00
[SwiftUI]NavigationViewのnavigationBarTitle位置に画像とテキストを両方入れる方法
実装するもの
SwiftUIにおけるSwiftUI NavigationViewでListからNavigationLinkで遷移する方法ではnavigationBarTitleの使い方についても触れましたが、ここでは以下の実装をする為に必要な方法をご紹介したいと思います。
Code
ContentViewstruct ContentView: View { var body: some View { VStack { TitleView(image: Image("SwiftUI"),titleName: "SwiftUI") .frame(width: UIScreen.main.bounds.width * 0.95, height: UIScreen.main.bounds.height * 0.1) List(1..<100) { num in NavigationLink(destination: Text("Lesson\(num)")) { Text("Lesson\(num)") } } } } }Code
TitleViewstruct TitleView: View { let image: Image let titleName: String var body: some View { HStack { VStack(alignment: .leading) { image .resizable() .frame(width: 50, height: 50) } ZStack { Text("\(titleName)") .fontWeight(.black) .foregroundColor(Color.black) .font(.largeTitle) } Spacer() } .padding() .background(Color.white) .clipShape(RoundedRectangle(cornerRadius: 10)) } }解説
現段階でnavigationBarTitleにImageとTextを入れる方法がない(と思う)為、TitleViewを作り、最上部にVstackで配置する事でそれらしく見せる事ができます。また.frame(width: UIScreen.main.bounds.width * 0.95, height: UIScreen.main.bounds.height * 0.1)では高さや幅をデバイスのサイズによって変更する為、綺麗な設計をする上では非常に有効ですので是非ご活用ください。
また使用するImageはassetにあらかじめご用意ください。最後に
普段は個人でSwiftUIでアプリを作っています。ハードも作るの大好きです。
よければ下記もチェックして下さい♪
https://twitter.com/oka_yuuji
note
https://note.com/oka_yuuji
- 投稿日:2020-09-06T21:03:17+09:00
[Swift]Eurekaライブラリのカスタマイズ集
はじめに
Eurekaライブラリとは
Eurekaライブラリとは、iOSの入力フォームなどを、高速かつ簡単に作成するためのライブラリである。
https://github.com/xmartlabs/Eureka背景
このライブラリの基本的な使い方は様々な記事で紹介されている。しかし、応用的な使い方や、マイナーなカスタマイズは中々載っていなかったため、私自身カスタマイズに苦労した。
私この経験を記事にすることで、皆様のお役に立てるのではないかと思い投稿させて頂いた。この記事の主なターゲット
- Eurekaライブラリの基本的な使用方法を知っている方
- Swift初心者の方
- Eurekaを使用して間も無く、もっと活用できるようになりたいと感じている方
編集履歴
- 2020年9月6日(日):本記事を投稿
カスタマイズ集
LabelRowにDisclosure Indicatorを表示
EurekaSample.swiftclass EurekaSample: FormViewController { override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(LabelRow() { $0.cell.accessoryType = .disclosureIndicator }) }) } }LabelRowにSub Titleを追加
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController { override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(LabelRow() { $0.title = "title" $0.cellStyle = UITableViewCell.CellStyle.subtitle }.cellUpdate { cell, _ in cell.detailTextLabel?.text = "sub title" cell.detailTextLabel?.textColor = UIColor.systemGray }.onCellSelection { cell, row in self.navigationController?.pushViewController(UIViewController(), animated: true) }) }) } }PushRowの選択先のVCのSectionを複数にする
無理やりです。
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController { private let dataList = [String]() override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(PushRow<String>("tag") { $0.title = "title" // オプションで追加はしない //$0.options }.onPresent { from, to in // デフォルトのセクションタイトルを消す to.form.allSections.first?.header = nil // セクション追加 let section1 = Section("section1") section1.tag = "section1" let section2 = Section("section2") section2.tag = "section2" for data: String in self.dataList { section1.append(LabelRow() { $0.title = data }.onCellSelection { cell, row in // デフォルトのPushRowを同じ挙動をさせる self.navigationController?.popViewController(animated: true) (self.form.rowBy(tag: "tag") as! PushRow<String>).value = data }) } to.form.append(section1) to.form.append(section2) }) }) } }文字入力系RowのUIToolbarをカスタマイズ
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController, UITextFieldDelegate { private var activeTextField: UITextField? = nil // TextFieldが選択された時 func textFieldDidBeginEditing(_ textField: UITextField) { // textFieldの参照先をメンバ変数で保持しておく self.activeTextField = textField } // Keyboardの上のUIToolbarの完了ボタン押下時の処理 @objc private func focusDelete() { // 参照先を保持しているTextFieldのFocusを外す activeTextField?.resignFirstResponder() } override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(TextRow() { $0.title = "title" }.cellUpdate { cell, row in // ここでこのRowのTextFieldのDelegateをセットさせる cell.textField.delegate = self }) }) } // カスタマイズUIToolbar(右側にDone buttonのみを配置) override func inputAccessoryView(for row: BaseRow) -> UIView? { let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 0)) toolbar.sizeToFit() var items = [UIBarButtonItem]() items.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)) // DoneのActionを設定 let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(focusDelete)) items.append(doneButton) toolbar.items = items return toolbar } }SectionのHeader、Footerをカスタマイズ
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController { override func viewDidLoad() { super.viewDidLoad() let section = Section() section.footer = { var footer = HeaderFooterView<UIView>(.callback({ let view = UIView(frame: CGRect(x: 10, y: 0, width: self.view.bounds.width - 20, height: 100)) return view })) footer.height = { 180 } footer.onSetupView = { view, _ in view.preservesSuperviewLayoutMargins = false let label = UILabel(frame: CGRect(x: 10, y: 0, width: self.view.bounds.width - 20, height: 100)) label.font = UIFont.systemFont(ofSize: 13) label.numberOfLines = 0 label.textAlignment = .left label.text = "footer description" label.sizeToFit() view.addSubview(label) } return footer }() form.append(section) } }曜日のPickerInlineRow
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController { override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(PickerInlineRow<Weekday>() { $0.options = [.sunday, .monday, .tuesday, .wednesday, .thursday, .friday, .saturday] $0.displayValueFor = { guard let weekday = $0 else { return nil } return weekday.rawValue + "曜日" } }) }) } } enum Weekday: String { case sunday = "日" case monday = "月" case tuesday = "火" case wednesday = "水" case thursday = "木" case friday = "金" case saturday = "土" }月と日のみのPickerInlineRow
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController { override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(DoublePickerInlineRow<String, String>() { $0.firstOptions = { return (1...12).map { String($0) + "月" } } $0.secondOptions = { month in var days = [String]() switch month { case "4月", "6月", "9月", "11月": days = (1...30).map { String($0) + "日" } case "2月": days = (1...29).map { String($0) + "日" } default: days = (1...31).map { String($0) + "日" } } return days } $0.displayValueFor = { guard let monthDay = $0 else { return nil } return monthDay.a + monthDay.b } }) }) } }おわりに
以上になります。万が一ミスなどをお気づきになりましたら、お手数をおかけしてしまい申し訳ありませんが、コメントなどでお知らせして頂けると助かります。
今後も他のカスタマイズが気付き次第更新したいと思います。
- 投稿日:2020-09-06T21:03:17+09:00
[Swift]Eurekaライブラリの応用カスタマイズ集
はじめに
Eurekaライブラリとは
Eurekaライブラリとは、iOSの入力フォームなどを、高速かつ簡単に作成するためのライブラリである。
https://github.com/xmartlabs/Eureka背景
このライブラリの基本的な使い方は様々な記事で紹介されている。しかし、応用的な使い方や、マイナーなカスタマイズは中々載っていなかったため、私自身カスタマイズに苦労した。
私この経験を記事にすることで、皆様のお役に立てるのではないかと思い投稿させて頂いた。この記事の主なターゲット
- Eurekaライブラリの基本的な使用方法を知っている方
- Swift初心者の方
- Eurekaを使用して間も無く、もっと活用できるようになりたいと感じている方
編集履歴
- 2020年9月6日(日):本記事を投稿
カスタマイズ集
LabelRowにDisclosure Indicatorを表示
EurekaSample.swiftclass EurekaSample: FormViewController { override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(LabelRow() { $0.cell.accessoryType = .disclosureIndicator }) }) } }LabelRowにSub Titleを追加
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController { override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(LabelRow() { $0.title = "title" $0.cellStyle = UITableViewCell.CellStyle.subtitle }.cellUpdate { cell, _ in cell.detailTextLabel?.text = "sub title" cell.detailTextLabel?.textColor = UIColor.systemGray }.onCellSelection { cell, row in self.navigationController?.pushViewController(UIViewController(), animated: true) }) }) } }PushRowの選択先のVCのSectionを複数にする
無理やりです。
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController { private let dataList = [String]() override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(PushRow<String>("tag") { $0.title = "title" // オプションで追加はしない //$0.options }.onPresent { from, to in // デフォルトのセクションタイトルを消す to.form.allSections.first?.header = nil // セクション追加 let section1 = Section("section1") section1.tag = "section1" let section2 = Section("section2") section2.tag = "section2" for data: String in self.dataList { section1.append(LabelRow() { $0.title = data }.onCellSelection { cell, row in // デフォルトのPushRowを同じ挙動をさせる self.navigationController?.popViewController(animated: true) (self.form.rowBy(tag: "tag") as! PushRow<String>).value = data }) } to.form.append(section1) to.form.append(section2) }) }) } }文字入力系RowのUIToolbarをカスタマイズ
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController, UITextFieldDelegate { private var activeTextField: UITextField? = nil // TextFieldが選択された時 func textFieldDidBeginEditing(_ textField: UITextField) { // textFieldの参照先をメンバ変数で保持しておく self.activeTextField = textField } // Keyboardの上のUIToolbarの完了ボタン押下時の処理 @objc private func focusDelete() { // 参照先を保持しているTextFieldのFocusを外す activeTextField?.resignFirstResponder() } override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(TextRow() { $0.title = "title" }.cellUpdate { cell, row in // ここでこのRowのTextFieldのDelegateをセットさせる cell.textField.delegate = self }) }) } // カスタマイズUIToolbar(右側にDone buttonのみを配置) override func inputAccessoryView(for row: BaseRow) -> UIView? { let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 0)) toolbar.sizeToFit() var items = [UIBarButtonItem]() items.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)) // DoneのActionを設定 let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(focusDelete)) items.append(doneButton) toolbar.items = items return toolbar } }SectionのHeader、Footerをカスタマイズ
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController { override func viewDidLoad() { super.viewDidLoad() let section = Section() section.footer = { var footer = HeaderFooterView<UIView>(.callback({ let view = UIView(frame: CGRect(x: 10, y: 0, width: self.view.bounds.width - 20, height: 100)) return view })) footer.height = { 180 } footer.onSetupView = { view, _ in view.preservesSuperviewLayoutMargins = false let label = UILabel(frame: CGRect(x: 10, y: 0, width: self.view.bounds.width - 20, height: 100)) label.font = UIFont.systemFont(ofSize: 13) label.numberOfLines = 0 label.textAlignment = .left label.text = "footer description" label.sizeToFit() view.addSubview(label) } return footer }() form.append(section) } }曜日のPickerInlineRow
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController { override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(PickerInlineRow<Weekday>() { $0.options = [.sunday, .monday, .tuesday, .wednesday, .thursday, .friday, .saturday] $0.displayValueFor = { guard let weekday = $0 else { return nil } return weekday.rawValue + "曜日" } }) }) } } enum Weekday: String { case sunday = "日" case monday = "月" case tuesday = "火" case wednesday = "水" case thursday = "木" case friday = "金" case saturday = "土" }月と日のみのPickerInlineRow
EurekaSample.swiftimport Eureka class EurekaSample: FormViewController { override func viewDidLoad() { super.viewDidLoad() form.append(Section() { $0.append(DoublePickerInlineRow<String, String>() { $0.firstOptions = { return (1...12).map { String($0) + "月" } } $0.secondOptions = { month in var days = [String]() switch month { case "4月", "6月", "9月", "11月": days = (1...30).map { String($0) + "日" } case "2月": days = (1...29).map { String($0) + "日" } default: days = (1...31).map { String($0) + "日" } } return days } $0.displayValueFor = { guard let monthDay = $0 else { return nil } return monthDay.a + monthDay.b } }) }) } }おわりに
以上になります。万が一ミスなどをお気づきになりましたら、お手数をおかけしてしまい申し訳ありませんが、コメントなどでお知らせして頂けると助かります。
今後も他のカスタマイズが気付き次第更新したいと思います。
- 投稿日:2020-09-06T20:46:32+09:00
[SwiftUI]NavigationViewでListからNavigationLinkで遷移する方法
今回の記事で実装できるもの
SwiftUIでNavigationViewを使えば本当に簡単に以下の様なものが作れます。
以下ではこの実装の解説をしていきます。
Code
struct ContentView: View { var body: some View { NavigationView { List(1..<100) { num in NavigationLink(destination: Text("Lesson\(num)")) { Text("Lesson\(num)") } }.navigationBarTitle("SwiftuUI") } } }これだけ少ないコードで上記の様な
・タイトル
・リスト
・遷移
を実装する事ができます。
ではどの様に実装しているか少し解説していきたいと思います。解説
NavigationView
・NavigationLink
・navigationBarTitle
はもちろんですが、その他にも
・navigationBarItems
など使用するには必須。List
(1..<100)でリストの個数を定義しています。今回は1〜99のリストを作成して数字はnumに返しているのでnumもそのままList内で使う事がきますのでText内などでも(\num)で使用する事ができます。
NavigationLink
destination以降でリンク先の表示するものを定義する事ができます。今回はテキストですがimageはもちろん、Viewを指定して画面を遷移させる事も可能です。
navigationBarTitle
("")内に書いたテキストをタイトル位置(画面の上部)に表示させる事ができます。
大きさは
("SwiftuUI", displayMode: .large)
displayModeの後に
large
inline
automatic
で指定する事ができます。以上です。是非参考になれば嬉しいです。
最後に
普段は個人でSwiftUIでアプリを作っています。ハードも作るの大好きです。
よければ下記もチェックして下さい♪
https://twitter.com/oka_yuuji
note
https://note.com/oka_yuuji
- 投稿日:2020-09-06T19:57:29+09:00
NSScrollViewのセンタリング(fullSizeContentView, magnification対応付き)
NSScrollView の定番の質問に「コンテンツのセンタリングをしたい」というのがあります。
歴史のあるクラスだけあって Stackoverflow なんかでもちょいちょい転がっている頻出パターンなのですが、fullSizeContentView や magnification まで考慮した現代的な回答が見つからなかったためまとめます。
解法
結論だけ先にいうと、大枠では他所の回答と同じく NSClipView のカスタマイズによって解決します。このビューをデフォルトの NSClipView の代わりに使うことで自動的に中身のコンテンツがセンタリングされます。
(サンプル: https://github.com/ryutei/CenteringContentViewSample)CenteringClipView.swiftimport Cocoa class CenteringClipView: NSClipView { override func constrainBoundsRect(_ proposedBounds: NSRect) -> NSRect { let base = super.constrainBoundsRect(proposedBounds) guard let documentFrame = self.documentView?.frame else { return base } let frame = self.frame let mag = frame.width / proposedBounds.width // #1 let insets = self.enclosingScrollView!.contentInsets // #2 // #3 let deltaX = max(frame.width/mag-documentFrame.width, 0.0) let deltaY = max(frame.height/mag-insets.top/mag-documentFrame.height, 0.0) var ret = base // #4 ret.origin.x -= deltaX/2.0 ret.origin.y -= deltaY/2.0 return ret } }解説
まず NSClipView.constrainBoundsRect(_:) は何をするメソッドなのかというと、基本的にはスクロール(や拡大縮小)の変化に対応して「これ以上行くな」という範囲の制約をかけるためのものです。
例えばマウスのホイールを回すとその変化量に応じてビューがスクロールビューの中を移動するわけですが、何も制限がかかっていなければコンテンツはやがて画面から外れて無限遠に流れていくことになります。そこで、フレームやらコンテンツの大きさやらを見て、もう端まで来ていると判断したら境界値でストップさせるよう値を調整するわけです。
引数に渡される proposedRect は取り敢えず何も考えずに変化量を反映させてみた場合の値であり、コンテンツが実際に適切な位置にあるかどうかは考慮されていません。なのでこちらがすることは proposedRect の値を参考にしつつ、都合のいい rect に変形して返してやる作業になります。
で、これで具体的に何を計算するのかというと、NSClipView の bounds を返します。NSScrollView はユーザー定義の documentView に対して、一種の選択領域となる contentView (NSClipView) を動かすことで documentView のどの範囲を表示するか制御しています。bounds が移動するとその分 documentView はオフセットされ、sizeを変えると documentView の倍率が変わることになります。なので、ビューが空いている時のセンタリングをしたければ bounds の origin を余白のサイズだけ移動してやればいいことになります。
NSClipView は NSScrollView によって実際に表示される範囲に合わせて大きさが設定されるので、余白の量は基本的には自身の frame と documentView の frame の差ということになります。これが負の値になったら余白がないということなので、この場合は 0.0 にするのが普通です(#3)
ちなみに、bounds に対するジオメトリの変化は documentView に対して逆向きに作用するという点に注意してください。上でも説明したとおり、NSClipView は documentView の選択領域を定義するという考え方なので、bounds が正の方向に移動するということはドキュメント上の選択領域が原点から反対側の角に向かって移動することに相当します。つまり元々の原点は(下向きが正なら)画面外の左上方向に遠ざかっていくということであり、感覚的には負のオフセットがかかるわけです(#4)。
同様に拡大縮小も bounds が広くなると documentView の大きさは小さくなります。これは広角レンズを使うと一度に写る範囲が広くなる代わり、写るものの大きさが小さくなるのをイメージすると分かりやすいと思います。magnificationとfullSizeContentView
さて、原則としてはこれでいいのですが、計算をややこしくするのが magnification と fullSizeContentView の存在です。
magnification
まず magnification ですが、NSScrollView の magnification が変化すると、NSClipView の bounds はその逆数の倍率補正が入ります。したがってオフセット量も1/倍率にしてやる必要があるわけです。ただし、倍率の影響を受けるのはあくまで NSClipView なので、bounds の計算は倍率を考慮しなければならない が documentView の frame は特に倍率の影響を受けないという点には注意する必要があります。したがって倍率補正込みの余白計算を式にすると次のようになります。
- (画面上の)余白 = contentView.frame - documentView.frame * 倍率
- bounds上のオフセット = 余白 / 2 / 倍率
上のコードで言うと #3 のdeltaを計算している辺りです。なおコード上はいちいち倍率をかけたり割ったりするのは無駄なので、オフセットの/倍率は余白側に取り込んでまとめています。
fullSizeContentView
次に fullSizeContentView です。まずこれは何かというと、iOS や Mac でも Safari なんかがやっている、タイトルバーの下にコンテンツのブラー表示が潜り込むアレです。ビューの構造上はウィンドウ全体がコンテンツの表示領域となり、その上にタイトルバーをオーバーレイするような実装になっています。
NSScrollView、NSClipView ともタイトルバーを含むウィンドウ全領域の大きさを持っている点に注意してください。
これはフルサイズ表示の趣旨を考えれば当たり前で、タイトルバーの下に何かを表示するためにはその下にコンテンツをスクロールできる必要があるわけです。しかし、ストレートにそれをやってしまうとタイトルバーの下にある内容が見えないということになるので、コンテンツがスクロールアウトした場合にのみタイトルバーの下に流れるという特殊な動きをとることになります。
実はこれはセンタリング計算と同じ理屈です。要するにタイトルバーの分だけ縦方向の余白を多く取ることで全体を下側にオフセットするというのが fullSizeContentView の仕掛けなわけです。このタイトルバー(とツールバー)の大きさは NSClipView と NSScrollView が持っている contentInsets で取得できます。真面目に計算するなら四方の insets をちゃんと計算したほうがいいとは思うのですが、現実問題としてタイトルバーの潜り込み以外で insets を使うことはあまりないように思うので、ここでは insets.top のみを対象としています。ただし、NSClipView のデフォルト実装も contentInsets はちゃんと考慮してくれるので、通常は余白計算だけ考慮すれば問題ありません。上のコードで言うと、#3の deltaY の計算で frame から insets.top を引く(つまり画面に見えている大きさを計算している)処理がこれに該当します。
アニメーション
最後に実装上のトラップについて触れておきます。上のコードの #1 では拡大率を取るのに NSScrollView の magnification を使わず、わざわざ frame と proposedBounds の比で拡大率を計算していますが、実はこれは必然的な理由によるものです。
NSScrollView.animator().setMagnification(_:centeredAt:) によって拡大縮小のアニメーションを行う場合、proposedBounds で要求される拡大率と NSScrollView がプロパティとして持っている拡大率(magnification)は初期値が異なっており、アニメーションがおかしな挙動を示します。そのため本当の拡大率を取得するためには必ず proposedBounds の値を参照する必要があるわけです。
アニメーション周りは他にもトラップがあって、contentInsets の取得を enclosingScrollView ではなく NSClipView から直接取得すると、設定されるタイミングが異なるのかアニメーションにガタが出るようです。現状でもすべての場合を網羅できているか正直自信がないのですが、一般的な状況については問題のない挙動になっていると思います。
- 投稿日:2020-09-06T19:09:00+09:00
[Swift] 実務的Tips: Tupleのswitchで条件マトリックスをもれなく分岐させる
今日からでもすぐに取り入れられて、
- コードをよりクリーンにできる、とか
- 工数を削減できる、とか
そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。前提環境
- Xcode 11.3.1
- Swift 5.1.3
Tupleのswitch
var isWeekday = false var isChildlen = false var isSenior = false switch (isWeekday, isChildlen, isSenior) { case (true, true, _): print("平日・子ども料金") case (true, _, true): print("平日・シルバー料金") case (true, _, _): print("平日・大人料金") case (false, true, _): print("休日・子ども料金") case (false, _, true): print("休日・シルバー料金") case (false, _, _): print("休日・大人料金") }
- Swiftの
switchは全ての組み合わせを網羅しないとコンパイルエラーにしてくれます。- それはTupleを
switchする場合も同様で、全要素が取りうる全組み合わせを網羅させることが容易になります。- 可読性はあまりよくないので、「全ての組み合わせを網羅する」ことを最優先にしたい場合以外は避けた方が良い書き方かもしれません。
- 投稿日:2020-09-06T19:06:47+09:00
AtCoderにSwiftで挑む時にPlaygroundを使うための小ワザ
AtCoderにて、Swift 5(執筆時点では5.2.1)が使えるようになりました。
プログラミング・コンテストのコーディングにはブラウザ上でコードを実行できるWebサービスを使うことが一般的かもしれませんが、、、
Playgroundだと補完機能とかコードスニペットとかいろいろと便利なので、Playgroundを使いたい。
しかし、Playgroundでは、AtCoderの問題の前提になっている標準入力が使えないのです。
(私の認識が間違っていたら教えてください。。。)そこで、私なりの小ワザを考え、実際にコンテストにSwiftで挑戦して、ちゃんとコードを提出できたので共有します。
結果はイマイチだったので触れません(笑サンプルコード
過去問の ABC 086 A - Product の回答コードです。
詳細はリンク先をみていただきたいですが、要するに以下のような問題です。
- 標準入力より引数が"a b"という形式で与えられる。
- a × bが奇数なら"Odd"と、 偶数なら"Even"と出力する。
ATCoder.playground// for TEST var inputs = ["3 4"] func readLine() -> String? { guard !inputs.isEmpty else { return nil } return inputs.removeFirst() } // for TEST import Foundation func readInts() -> [Int] { return readLine()!.split(separator: " ").map { Int($0)! } } func main() { let ab = readInts() let isEven = ab[0] * ab[1] % 2 == 0 print(isEven ? "Even" : "Odd") } main() // Playgroundで実行すると出力コンソールに"Even"と表示される解説
// for TESTコメントで囲まれたコードブロックは、Swiftの標準入力読み込み関数readLine()を同じ関数名で自作することで、上書きと言いますか、hookしてしまう、という意図のコードです。var inputs = ["3 4"]は、問題で標準入力から与えられる引数になります。この配列の要素を変えてテストをします。- 問題によっては標準入力で複数行の引数が与えられるということがあります。その場合は
var inputs = ["2", "3 4", "5 6"]のように定義します。- コンテストにコードを提出する時は、
// for TESTコメントで囲まれたコードブロック以外をコピペすればOKです。参考リンク
- 投稿日:2020-09-06T17:03:50+09:00
Firebase Cloud Messagingを使ってアプリに通知機能を実装する
はじめに
本記事はFirebaseのプロジェクトが作成済みでアプリにすでにFirebaseを導入していることを前提とした内容となっています。また、iOSアプリを対象にしています。
Firebase Cloud Messaging(以降FCM)とは公式ドキュメントによるとFirebase Cloud Messaging(FCM)は、メッセージを無料で確実に送信するためのクロスプラットフォーム メッセージング ソリューションです。
ということです。通常アプリにプッシュ通知を送るにはアプリの設定以外にもAPNs(GCM)に通知送信をするためのアプリケーションサーバーを用意しないといけません。(Pusherなどのアプリで代用も可能)FCMであればAPNsに通知を送るための設定をFirebaseのコンソールから簡単に行うことができます。
プロジェクトの設定
Cloud Messagingに必要なライブラリのインストール
// Podfile pod 'Firebase/Messaging' // install $ pod installプッシュ通知に必要なCapabilityを設定
- TARGETS -> Signing & Capability -> + を押下
- その中からBackground ModesとPush Notificationsをダブルクリックで追加
- Background ModesのRemote notifiationsにチェック
この時点でプロジェクト名.entitlementsが追加されます。 中身を見ると
development用が追加されています。他の記事を見るとこれをコピーして本番用を用意してたりしたのですが、根拠となるソースが見つからなかったのでAppleのドキュメントを漁ったところ
Xcode sets the value of the entitlement based on your app's current
provisioning profile. For example, if you're using a development provisioning
profile, Xcode sets the value to development.プロジェクトに設定されているProvisioning Profileによって自動的に変更されるようです。
試しにProvisoning ProfileをDistributionに変えたんですが、書き換わることはありませんでした。
どうやらArchiveするときに自動的に値をセットしてくれる仕様のようです。
https://stackoverflow.com/questions/42292363/aps-environment-is-always-development
試しにアーカイブした中身を確認したところ
変更されていました。
また、AppStoreConnectにアップロードしたバイナリも確認しましたが、productionになっていました。
Push用のCertificateを作成
すでにアプリのIdentifiersが用意されている前提の説明となります。はじめから作る場合はこちらが参考になるかと思います。
- キーチェーンアクセスより証明書を発行(本番・開発用の計2つ)
- Identifiersをクリック
- Push Notifications -> Configure
![]()
- Development SSL Certificate -> Create Certificate
![]()
- 先程作った証明書を使ってCertificateを作成
- 本番も同様に作成
これら2つをダウンロード
~.cerをダブルクリックし、キーチェーンに登録
それぞれ右クリックし、「〜を書き出す」
FirebaseのCloud MessagingにAPNs証明書を登録
Firebaseコンソールの歯車を押して「プロジェクトを設定」
Settings -> Cloud MessagingからAPNs証明書の項目までスクロールし、先程作った証明書をアップロードします
AppDelegate.swiftに通知を受け取るための処理を追加
AppDelegate.swiftに以下を追加
import Firebase import FirebaseMessaging class AppDelegate: UIResponder, UIApplicationDelegate { ... func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. FirebaseApp.configure() // Optional Messaging.messaging().delegate = self UNUserNotificationCenter.current().delegate = self // 通知の許可をユーザーに要求する UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound],completionHandler: { granted, error in guard error == nil else { return } if granted { // registerForRemoteNotificationsは必ずメインスレッドで実行しなければならない DispatchQueue.main.async { // Appleプッシュ通知サービスを介してリモート通知を受信するための登録を行う application.registerForRemoteNotifications() } } }) return true } ... } // Optional extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) { // テスト送信用のトークン取得 print("Firebase registration token: \(fcmToken)") } } extension AppDelegate : UNUserNotificationCenterDelegate { // アプリがフォアグラウンドで通知を受け取ったとき func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { let userInfo = notification.request.content.userInfo // Print full message. print(userInfo) // Change this to your preferred presentation option completionHandler([[.alert, .sound]]) } // ユーザーが通知バナーをタップしたとき func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo // Print full message. print(userInfo) completionHandler() } }実装する上で何点か注意点があります。
まず一つ目として、iOS10未満を対象とするかで通知を受信したときのデリゲートメソッドの実装が異なります。
今回iOS10未満を対象としないため上記実装となります。もし、iOS10未満も対象にする場合は公式ドキュメントを参考に場合分けが必要です。
二つ目の注意点として、registerForRemoteNotificationsを実行する際はメインスレッドで行う必要があるということです。メインスレッドでの実行を明示しない場合は以下警告がでます。
三つ目はMessagingDelegateの実装です。こちらは実装しなくても通知は受け取れます。
ただ、Firebaseコンソール上での通知送信は開発・本番の区別なく送信されるため、テスト送信をしたい場合にはFCMトークンを指定してテスト送信をする必要がありました。
そのため、本番に送る前に手元で通知内容を確認したい場合にはFCMトークンを取得すると便利です。FirebaseコンソールからPushを送る
FirebaseコンソールのCloud Messaging -> 通知の作成から新たな通知を発行できます。
ここの「テストメッセージを送信」を選ぶと特定の端末に通知を送ることができます。
先程のAppdelegate内のデリゲートでプリントしたFCMトークンをここに追加して「テスト」を押せば通知を受け取れます。
テスト送信ができればあとは通知設定をしていき、最後に「公開」を押せば本送信されます。
参考
- 投稿日:2020-09-06T16:54:16+09:00
【Swift・Objective-C】iframeを含むWebViewを表示する際にshouldStartLoadWithRequest:が呼ばれてしまう時の対処法
iframeのURLに反応するshouldStartLoadWithRequestに対する対処法
以下のようにメインコンテンツとiframeかを判定することにより、iframeにshouldStartLoadWithRequestが反応しても対処ができます。
Swift
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool { if request.URL.absoluteString == request.mainDocumentURL?.absoluteString { // メインコンテンツ(読み込もうとしているWebView)の読込時の処理 } // iframeのURLの読み込み時の処理 }Objective-C
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { if ([request.URL.absoluteString isEqualToString:request.mainDocumentURL.absoluteString]) { // メインコンテンツ(読み込もうとしているWebView)の読込時の処理 } // iframeのURLの読み込み時の処理 }
- 投稿日:2020-09-06T16:19:32+09:00
[Swift5] 部分文字列を文字数から取得するときの注意点。
Swiftの
Stringは真剣にunicodeを扱っているらしく、大変難しいです。
参考:Swift の文字列の長さ - Qiita部分文字列(ex:「アイウエオ」の「イウエ」)の取得も例外ではありません。Swift4までのやり方とは変わってしまい、調べてもなかなか出てこないのでメモしておきます。
まず、求めるのは「
i番目の文字からj番目の文字まで」とします。例えばi=1, j=3でstring = "アイウエオ"ならばpartialString = イウエとなります。
以下は誤りです。let partialString = [String.Index(utf16Offset: i, in: string)...String.Index(utf16Offset: j, in: string)]私は最初うっかりしてこの方法を使っていたのですが、これだと次のような例でバグが起こります。
let string = "✨??✨" let partialString = [String.Index(utf16Offset: 1, in: string)...String.Index(utf16Offset: 2, in: string)] print(partialString) //"?"なぜこうなってしまうのかというと、
utf16Offsetとしっかり書いてある通り、あくまでここ得ているIndexはutf16でのものだからです。実際print("✨??✨".utf16.count) //6なので、納得のいく結果でした。
確実に文字数単位で取り出したいときは、一度文字ごとに分割してから改めて次のようにします。let string = "✨??✨" let partialString = String(string.map{$0}[1...2]) print(partialString) //"??"なかなか危ないところなので気をつけましょう。
- 投稿日:2020-09-06T09:40:13+09:00
背景に動画を流す方法
背景に動画を置く方法
画面の背景に動画をリピートで再生させ続けたい場合
viewController.swiftimport UIKit import AVFoundation class PopupDetailViewController: UIViewController { var player = AVPlayer() let path = Bundle.main.path(forResource: "Sample", ofType: "mov") override func viewDidLoad() { super.viewDidLoad() player = AVPlayer(url: URL(fileURLWithPath: path!)) player.play() let playerLayer = AVPlayerLayer(player: player) // フレームの大きさを決める playerLayer.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height) playerLayer.videoGravity = .resizeAspectFill playerLayer.repeatCount = 0 playerLayer.zPosition = -1 view.layer.insertSublayer(playerLayer, at: 0) // リピートさせる NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { (notification) in self.player.seek(to: .zero) self.player.play() } }動画の保存場所
ここにファイルを移せば再生される
- 投稿日:2020-09-06T09:33:19+09:00
SwiftUIをジェネリクスでプレビューしやすくする
SwiftUIでアプリを構築していてプレビュー機能をフル活用しないことにはSwiftUI使ってる旨味を最大限に引き出せませんよね。
このWWDC動画でのプレビュー機能の活用方法がとってもわかりやすく実用的だったので、要点だけまとめて記事にします。
シンプルデータとリッチデータ
動画で語られているのが、アプリで取り扱うデータは主に2つに分類できると言及されています。
- リッチデータ ... CoreData, RealmやCloudKitなどの情報やサーバ側にしか無いデータなど
- シンプルデータ ... Stringなどのプリミティブなデータ型、構造体など
シンプルデータは生成や取得が容易でリッチデータは生成や取得へのレイヤーが深かったり手続きが面倒なものといったところでしょうか。
動画ではSwiftUIのView層は極力シンプルデータで構築しましょうと言っています。
仮にRealmやCloudKitのデータをViewで表示する場合であったとしてもリッチデータをそのままバインドするのではなくprotocolなどで抽象化し、シンプルデータとしてView側で表示するのを推奨しています。
リッチデータはテストやプレビューがしにくい
RealmやiCloudなどのリッチデータというのは生成がユーザ操作を経ないと出来ない場合や、ビューに必要ではない情報を多分に含んでいるケースなどが多く、テストやプレビューする時にはとても難儀です。
Realm上のデータをリストで表示みたいなユースケースを抽象化せずに実装していくとプレビューしにくいのは想像に難くないと思います。プレビューしずらい設計
import Foundation import SwiftUI import RealmSwift struct TodoView: View { @ObservedObject var viewModel: TodoViewModel var body: some View { List(self.viewModel.list, id: \.id) { todo in HStack { Image(systemName: todo.isComplete ? "checkmark.square" : "square") .foregroundColor(todo.isComplete ? .green : .secondary) Text(todo.title) } .padding(6) } } } struct ContentView_Previews: PreviewProvider { static let viewModel: TodoViewModel = TodoViewModel() static var previews: some View { TodoView(viewModel: viewModel) } } // MARK: viewModel class TodoViewModel: ObservableObject { @Published var list: [TodoEntity] private var dataSource: Realm init() { self.dataSource = try! Realm() self.list = self.dataSource.objects(TodoEntity.self).map { $0 } } } class TodoEntity: Object, Identifiable { @objc dynamic var id: String = UUID().uuidString @objc dynamic var title: String = "" @objc dynamic var isComplete: Bool = false }TodoリストをRealmで表示するケースでの実装パターン
このままだとPreviewProviderで空データのプレビューが表示されるだけでデータがある場合のプレビューが出来ません。
抽象化しておらず、Viewが詳細な実装に依存しているため、追加処理を実装するまでデータの一覧での確認するのが辛く、あまりよろしい設計とは言えません。今回のパターンは追加処理も簡単でプレビューしなくてもだいたいの画面はイメージできますが、より複雑なパターンや、RealmからCloudKitの載せ替えが発生した場合や、データをサーバーから取得する仕様に変更になった場合にサーバの実装が出来るまで待ち時間が発生してしまいます。
フロントエンドエンジニアとしてそれは由々しき問題なので、ビューからデータの発生源の関心を取り除きましょう。ジェネリクスを適用しモックに差し替えやすく
import Foundation import SwiftUI struct TodoView<T: TodoViewModelProtocol>: View { @ObservedObject var viewModel: T var body: some View { List(self.viewModel.list, id: \.id) { todo in HStack { Image(systemName: todo.isComplete ? "checkmark.square" : "square") .foregroundColor(todo.isComplete ? .green : .secondary) Text(todo.title) } .padding(6) } } } struct ContentView_Previews: PreviewProvider { class TodoViewModelMock: TodoViewModelProtocol { @Published var list: [TodoEntity] = [TodoEntity]() init() { self.list = [ TodoEntity(title: "first task", isComplete: true), TodoEntity(title: "second task", isComplete: false), TodoEntity(title: "third task", isComplete: true), ] } } static var previews: some View { TodoView<TodoViewModelMock>(viewModel: TodoViewModelMock()) } } // MARK: viewModel protocol TodoViewModelProtocol: ObservableObject { associatedtype ListData: TodoEntityProtocol var list: [ListData] { get set } } protocol TodoEntityProtocol { var id: String { get set } var title: String { get set } var isComplete: Bool { get set } } class TodoEntity: Object, Identifiable, TodoEntityProtocol { @objc dynamic var id: String = UUID().uuidString @objc dynamic var title: String = "" @objc dynamic var isComplete: Bool = false convenience init(title: String, isComplete: Bool) { self.init() self.title = title self.isComplete = isComplete } }
TodoViewModelProtocolとTodoEntityProtocolを新たに定義し、抽象化SwiftUIのView層はProtocolのみ知っている状態にし、ジェネリクスを用いて差し替え容易に
struct TodoView<T: TodoViewModelProtocol>: View { @ObservedObject var viewModel: T
PreviewProviderにモックデータ用のクラスを差し込めば実際のデータ状態に依存されずにプレビュー可能に。
モックデータ用のクラスはシンプルデータを自前で用意するだけで良くなりました。モッククラスを実行ファイルに同梱したくない場合は
Preview Contentフォルダに含めれば、製品版に不要なソースがバンドルされることもないので、とても有能です。SwiftUIのプレビューはデータ設計にも強力なツール
SwiftUIのプレビューは魅力的で、簡易なビューならすぐプレビューできますが、複雑なユースケースが絡んだ場合に安直に実装してしまうと途端にプレビューしにくくなってしまいます。
これをデメリットに感じてしまう人もいるかもしれませんが、UIフレームワークが実装者に設計を意識させる作り方になっているのだと感じました。抽象化のメリットを具体例で説明する時に毎回いい例を挙げれずに困っていましたが、今回のサンプルはいい例になるなと思い記事にしました。
UIKitは工夫しないとファットになってしまうアーキテクチャでしたが、SwiftUIは初期構想から実装者を良い設計に導くように作られているんだと思いました。
- 投稿日:2020-09-06T08:40:03+09:00
オプショナルへの予想を上回る気遣い
オプショナルの学習メモ
iOSアプリ開発において、オプショナルについて予想を上回る気遣いをしなければならないので、学習したことをメモしていきます。オプショナルについての内容を網羅している記事ではないのでご注意ください。
オプショナルとは?
マイページ設定の画面などには、入力すべき「必須項目」とそうでない「オプション項目」がある。学習している「オプショナル」とはここで言う「オプション項目」のことを指す。
何も値が入らない項目なので、値として「何もない = nil」をもてる変数に設定する。
※nilは空文字ではない。空文字は" "このスペース半角のように値が入っていることを表す。空文字はメモリに値を格納している。注意すべきは、何もない(= nil)をもてることが許されるということ
言い換えれば、普通は許されない。Swiftならではの設計。オプショナル変数の宣言方法
ポイントは型の後の?マーク!
変数にnilを代入var hoge: String? // = nilが代入できる※?だけでなく!をつけることもあるが、少し意味が異なるので注意。
オプショナル型の変数から値を取り出す
以下にコードの例を記述した。
エラーになる計算var age:Int? = 27 print(age + 1) //ageはオプショナル型であり整数型でないのでエラーオプショナル型の変数前に!を加える
これをアンラップ(unwrap)という。オプショナル型はnilを許容するようにラップ(wrap)されていて、それを剥がすというイメージ
※なお、以下で行っている値を取り出す処理を行う際に、変数の直後に"!"を記述して、アンラップすることを強制的アンラップ(Forced unwrap)というオプショナル型の変数から値を取り出すvar age:Int? = 27 print(age! + 1) // = 28なお、nilが代入されている時に"!"をつけると、クラッシュする
nilが代入されているvar age:Int? = nil print(age! + 1) //クラッシュつまり、オプショナル型の変数から値を取り出すときに"!"をつけることは、中身がnilではないことを保証しなければならない。
中身が絶対にnilではない時にしか"!"はつけてはいけない
とにかく上記が開発のミソ!!!!
中身がnilなのかどうかで設計の危険性が判断されたりする。オプショナル型のメリット
他の言語ではnilを参照するとアプリはクラッシュする。Swiftも同様であるが、変数宣言時にnilを含むことが許されるのでnilチェックが必要じゃなくなり開発の手間が省ける。
オプショナル関係のやつをまとめていく
大まかなことは上記に記した。その他の知識や役立つTIPSは以下にまとめていく。
オプショナルチェーン
プロパティやメソッドが数珠つなぎになった形から、オプショナル型の変数を取り出す際に変数の後に?をついているもの。取り出す変数がnilなら、指定したプロパティやメソッドは実行されない。
こんな形のやつ変数名?.メソッド.メソッド注意
オプショナルチェーンにおいて、オプショナル型の変数名の後に"!"をつけて値がnilだった場合はアプリがクラッシュする。オプショナルバインディング
オプショナル型の値に条件分岐をかけて値がnilかどうかで、処理が変わる
lf-let文の形になることが多いみたいオプショナルバインディングlet age: Int? if let me = age { print("合致しました") } else { print("nilです") //ageの値がnilなので、else以下が実行される }その他
guard let文を使うこと、map・flatmapメソッドとオプショナルの関係なども追記していきます。
- 投稿日:2020-09-06T02:10:28+09:00
CocoaPodsでPodをインストールしても使えない
Swift勉強し始めたのですが、podのインストールでめちゃくちゃハマったのでその怒りをQiitaに昇華します。
新鮮な怒りをお届け。ぶち当たったエラー
PKHUDとやらをインストールしようとして
$ pod installNo such module 'PKHUD'というエラーが出た。
解決方法
.codeprojではなく、.xcworkspaceからビルドする。stackoverflowを見ていたら、
Make sure you opened the .xcworkspace file in Xcode and not just the .xcodeproj file.
というコメントを発見し、
どうやらプロジェクトを新規で作成したときに生成される.xcodeprojから設定をいじるのと、pop install時に生成される.xcworkspaceから設定をいじるのは意味が違うということがわかった。.xcodeprojからではなく、.xcworkspaceからビルドすると、
import PKHUDの部分でエラーが出ずにビルドが成功しました。結局何が原因?
.codeprojは一つのプロジェクトを管理するファイル。対して.scworkspaceは複数のプロジェクトをまとめて管理するものらしい。CocoaPodsでインストールしたPodsは一つのプロジェクトとして管理されているので、Podsを使用したい場合は自分が作ったプロジェクト+Podsを管理しているプロジェクトの2つを一緒にビルドする必要がある。
なので、一つのプロジェクト(自分が作ったプロジェクト)を管理する
.codeprojから開いてもPodsのインポートでNo module Errorが出た。(ビルドがコケた)最後に
Podsを使った開発をするときは
.scworkspaceからビルドしよう
ルールを守って楽しく開発!!!※ (当記事はSwiftを始めて5日位の初心者です。間違いがあったら優しく指摘してね? )
参考文献
- 投稿日:2020-09-06T00:04:12+09:00
はじめての ReactorKit【実践編】
前回の概要編に続き、今回は実際に ReactorKit を使ったサンプル実装をしていきたいと思います。
作るアプリ
今回は Google Books API を使用したアプリを想定して実装していきたいと思います。仕様は下記の通りです。
※ 基本的には、ReactorKit 周りの実装が中心なので細かい API 処理などの実装部分などは省いていきます。
- 画面が開いたら、API からデータを取得して TableView に反映する
refreshButtonがタップされたら、データを更新する- API からデータを取得している最中は
activityIndicatorを表示し、取得が完了したら非表示にする作業開始??
まずは View のロジックを担う Reactor にそれぞれのイベントとデータを定義していきます。
import ReactorKit import RxSwift import RxCocoa // 後々 concat() 関数も使いたいのでインポート class BookListViewReactor: Reactor { enum Action { case load case refresh } // Mutation を定義しない場合は、Action が Mutation として扱われる enum Mutation { case setBooks([ServerBook]) case setLoading(Bool) } struct State { var books: [ServerBook] var isLoading: Bool } var initialState: BookListViewReactor.State = State(books: [], isLoading: false) }
Action・State・initialStateあたりは必須 Reactor プロトコルの定義で必須で、Mutationに関しては、Action をもとに実行される処理の具体的な結果の値を定義します。また、Mutationの定義は必須ではなく、定義が無い場合にはActionがMutationとして扱われます。次に
Actionをもとに処理を実行してMutationを返すためのmutate(action:)と、その受け取ったMutationをもとに新しいStateを返すreduce(state:, mutation:)関数を定義していきます。// BookListViewReactor func mutate(action: Action) -> Observable<Mutation> { switch action { case .load: return Observable.concat([ Observable.just(Mutation.setLoading(true)), // API から本の情報一覧を取得 BookService.sharedInstance.getAllBooks().flatMap { item in Observable.just(Mutation.setBooks(item.items))}, Observable.just(Mutation.setLoading(false)) ]) case .refresh: return Observable.concat([ Observable.just(Mutation.setLoading(true)), BookService.sharedInstance.getAllBooks().flatMap { item in Observable.just(Mutation.setBooks(item.items))}, Observable.just(Mutation.setLoading(false)) ]) } } func reduce(state: State, mutation: Mutation) -> State { switch mutation { case .setLoading(let isLoading): var newState = state newState.isLoading = isLoading return newState case .setBooks(let books): var newState = state newState.books = books return newState } }
mutate(action:)の中では、主にObservable.cancat()でMutaiontを返しています。今回 Action として定義したloadのように1つのイベントでloading の更新やAPI からデータを取得など複数の処理を行う必要があるので、RxCocoa が提供しているconcat()関数で Observable を直列に実行して、順次Mutatationを返しています。また、reduce(state:, mutation:)では受け取ったMutationと現在のStateをもとに新しいStateを発行して返しています。これで、Reactor 側の実装は完了したので、View の実装をしていきます。
まずは、使用する View(ViewController) を
ReactorKitが提供しているViewプロトコルに準拠させます。また、今回は、Storyboard を使用して View を作成していくので、StoryboardViewというプロトコルに準拠させます。これによって、ViewController の childViews が初期化されたタイミングでbind(reactor:)が呼ばれるようになります。import ReactorKit import RxSwift class BookListViewController: UIViewController, StoryboardView { @IBOutlet weak var refreshButton: UIButton! @IBOutlet weak var activityIndicator: UIActivityIndicatorView! @IBOutlet weak var tableView: UITableView! // ? 本来はより、Testable にするために Reactor の注入はクラス内では行いませんが、今回はサンプル実装のためこのままでいきます var reactor: BookListViewReactor? = BookListViewReactor() var disposeBag: DisposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() tableView.register(UINib(nibName: "BookTableViewCell", bundle: nil), forCellReuseIdentifier: "BookTableViewCell") } func bind(reactor: BookListViewReactor) { Observable.just(Void()) .map { Reactor.Action.load } .bind(to: reactor.action) .disposed(by: disposeBag) refreshButton.rx.tap .map { Reactor.Action.refresh } .bind(to: reactor.action) .disposed(by: disposeBag) // State binding. reactor.state .map { $0.isLoading } .distinctUntilChanged() // 値に変更があった場合にのみイベントを流す .map { !$0 } .bind(to: activityIndicator.rx.isHidden) .disposed(by: disposeBag) reactor.state .map { $0.books } .bind(to: tableView.rx.items(cellIdentifier: "BookTableViewCell", cellType: BookTableViewCell.self)) { index, book, cell in cell.set(book: book) } .disposed(by: disposeBag) } }基本的には、
disposeBagとbind(reactor:)の定義が必須になります。今回はサンプル実装なので、reactorへの反映をクラス内で行っていますが、本来はより Testable にするために、切り離す必要があります(初回表示の View の場合は、AppDelegate 内で reactor の反映を行うなど)。また、bind(reactor:)が呼ばれるのは、reactorへの反映が完了していて、かつviewDidLoadの後に呼び出されます。ですので、初回時に行う処理などをbind(reactor:)でrx.methodInvoked(#selector(viewDidLoad))のように Observe したいところですが、これは呼ばれないので初回時の処理はObservable.just(Void())で定義します(参考のIssue-comment)。こんな感じで、ReactorKit を使ってシンプルにアプリを作成することができました? また他の場面で使用することがあったらまた記事を書きたいと思います。
参考






















