- 投稿日:2021-03-21T20:58:14+09:00
ナビゲーションバーのカスタマイズ一覧(iOS)
はじめに
ナビゲーションバーをカスタマイズする方法を毎回忘れて実装に時間がかかるため、まとめました。
環境
- OS:macOS Big Sur 11.1
- Xcode:12.4 (12D4e)
- Swift:5.3.2
- iOS:14.4
ナビゲーションバーのカスタマイズ一覧
ナビゲーションバーのタイトルに画像をセットする
UINavigationItem.titleView
に画像をセットすることで、ナビゲーションバーのタイトルを画像にできます。MonsterIndexListViewController.swiftfinal class MonsterIndexListViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let imageView = UIImageView(image: UIImage(named: "navi_zukan")) imageView.contentMode = .scaleAspectFit self.navigationItem.titleView = imageView } }
テキスト 画像
UIImageView.contentMode
を.scaleAspectFit
にすると、画像の縦横比を変えずに表示できます。ナビゲーションバーの戻るボタンの画像を変更する
UINavigationBar
のbackIndicatorImage
とbackIndicatorTransitionMaskImage
を変更することで、戻るボタンの画像を変更できます。MonsterIndexListViewController.swiftfinal class MonsterIndexListViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let image = UIImage(named: "index_icon_back") self.navigationController?.navigationBar.backIndicatorImage = image self.navigationController?.navigationBar.backIndicatorTransitionMaskImage = image } }
画像未変更 画像変更 セットした画像は以下であり、
tintColor
が反映されてしまいます。
tintColor
を反映させない方法がわからなかったので、ご存じの方がいたら教えていただけると嬉しいです。2021/03/23, 追記
コメント で教えていただきました。対象画像のRender Asを
Default
からOriginal Image
に変えるとtintColor
が反映されなくなります。
ただ画像の位置が上に寄るのは直りません。
画像のサイズを調整すればかんたんに対応できます。
参考: https://sarunw.com/posts/how-to-change-back-button-image/#position
UIImage
の位置を変える方法もありますが、tintColor
が反映されるのと、UIImageView
の位置は変わらないので見切れることがあるため、ベターな方法ではなさそうです。
参考: https://stackoverflow.com/questions/29445644/vertically-center-backindicatorimage-in-swiftナビゲーションバーの戻るボタンのテキストを非表示にする
iOS 14以上の場合、 遷移元のビューコントローラー の
UINavigationItem.backButtonDisplayMode
を.minimal
にすることで、戻るボタンのテキストを非表示にできます。MainViewController.swiftfinal class MainViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.navigationItem.backButtonDisplayMode = .minimal } }
表示 非表示 戻るボタンの長押し時にビューコントローラーの
title
が表示されるので、戻るボタンを非表示にする場合でもセットするのが望ましいです。MainViewController.swiftfinal class MainViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + self.title = "メイン" self.navigationItem.backButtonDisplayMode = .minimal } }
タイトル未セット タイトルセット 戻るボタンのタイトルに半角スペースをセットする方法もよく見ますが、iOS 14以降では戻るボタン長押し時のメニューにタイトルが表示されなくなるので望ましくありません。
ナビゲーションバーの戻るボタンの色を変更する
UINavigationBar
のtintColor
を変更することで、戻るボタンの色を変更できます。MonsterIndexListViewController.swiftfinal class MonsterIndexListViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.navigationBar.tintColor = UIColor(named: "tekimon_khaki") } }
色未変更 色変更 一部のビューコントローラーのみナビゲーションバーの戻るボタンの色を変更する場合、戻し忘れに気をつけてください。
戻すには
nil
をセットします。MainController.swiftfinal class MainViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.navigationBar.tintColor = nil } }ナビゲーションバーの背景色を変更する
UINavigationBar
のbarTintColor
を変更することで、ナビゲーションバーの背景色を変更できます。MonsterIndexListViewController.swiftfinal class MonsterIndexListViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.navigationBar.barTintColor = UIColor(named: "tekimon_khaki") } }
背景色未変更 背景色変更 一部のビューコントローラーのみナビゲーションバーの背景色を変更する場合、戻し忘れに気をつけてください。
戻すには
nil
をセットします。MainController.swiftfinal class MainViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.navigationBar.barTintColor = nil } }ナビゲーションバーを透過する
UINavigationBar
のbackgroundImage
とshadowImage
を初期化することで、ナビゲーションバーを透過できます。MonsterIndexListViewController.swiftfinal class MonsterIndexListViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) self.navigationController?.navigationBar.shadowImage = UIImage() } }
非透過 透過 一部のビューコントローラーのみナビゲーションバーを透過する場合、戻し忘れに気をつけてください。
非透過にするには
nil
をセットします。MainViewController.swiftfinal class MainViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default) self.navigationController?.navigationBar.shadowImage = nil } }私が参考にしたサイトでは、
UINavigationBar
に以下のような拡張メソッドを実装していました。
意図が明確になっていいと思います。UINavigationBar+Transparency.swiftextension UINavigationBar { func enableTransparency() { setBackgroundImage(UIImage(), for: .default) self.shadowImage = UIImage() } func disableTransparency() { setBackgroundImage(nil, for: .default) self.shadowImage = nil } }ナビゲーションバーを非表示にする
UINavigationController.setNavigationBarHidden(_:animated:)
メソッドを呼び出すことで、ナビゲーションバーの表示/非表示を切り替えられます。MainViewController.swiftfinal class MainViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.setNavigationBarHidden(true, animated: false) } }
表示 非表示 ナビゲーションバーの表示/非表示が混在しているアプリの場合、画面を遷移するたびにナビゲーションバーの表示も切り替える必要があるので、
viewWillAppear(_:)
メソッド内で実行すべきです。ナビゲーションバーを表示 or 隠し忘れないように気をつけてください。
おまけ: スクリーンショットのアプリ
スクリーンショットに使ったアプリは、先日行われたHack Day 2021で私たちのチームが開発しました!
詳細は @hcrane14 さんがまとめているので、よかったら見てください。
https://note.com/hcrane/n/n98a8f1157390おわりに
これでナビゲーションバーをカスタマイズする案件が来ても安心です
![]()
self.navigationController
を変更すると他のビューコントローラーにも影響するのでviewWillAppear(_:)
で行って戻すのを忘れない、self.navigationItem
は変更しても他のビューコントローラーに影響しないのでviewDidLoad()
で行う、と覚えておくといいです(間違っていたらすみません)。参考リンク
- 投稿日:2021-03-21T20:03:00+09:00
TabeleView sectionの不要のスペースを除去
sectionFooterHeight と sectionHeaderHeight を0にするのと、
tableHeaderView, tableFooterView のそれぞれのviewにdummyのviewを代入するとよさそう。
dummyのviewは高さを0にしてしまうと不要スペースが消えないため注意ViewController.swiftimport UIKit class ViewController: UIViewController { let tableView: UITableView = UITableView(frame: .zero, style: .grouped) let list: [String] = ["あ", "い", "う", "え"] override func viewDidLoad() { super.viewDidLoad() configureLayout() } private func configureLayout() { tableView.delegate = self tableView.dataSource = self tableView.sectionHeaderHeight = 0 // 今回の場合、delegateで高さ指定しているため不要 tableView.sectionFooterHeight = 0 let dummyView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: .zero, height: 0.01))) // 0にすると高さが反映されない tableView.tableHeaderView = dummyView // 今回の場合、delegateで高さ指定しているため不要 tableView.tableFooterView = dummyView view.addSubview(self.tableView) self.tableView.translatesAutoresizingMaskIntoConstraints = false tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true tableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true tableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true } } extension ViewController: UITableViewDelegate, UITableViewDataSource { // MARK: tableView rows func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { case 2: return 0 default: return list.count } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell() cell.textLabel?.text = list[indexPath.row] return cell } // MARK: tableView Section // セクション数 func numberOfSections(in tableView: UITableView) -> Int { return 5 } // セクションタイトル func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return "SectionTitle " + String(section) } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let rect = CGRect.init(origin: .zero, size: CGSize(width: tableView.bounds.width, height: .zero)) let view = UIView(frame: rect) switch (section % 2) { case 0: view.backgroundColor = .blue default: view.backgroundColor = .green } return view } func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { switch section { case 2: return 0 default: return 50 } } }
- 投稿日:2021-03-21T16:03:49+09:00
【審査通りました!】【Swift UI編】初心者がApp StoreにiOS自作アプリを公開する過程
審査通りました!(2021/3/20)
なぜ書くのか
初めて作るアプリのリリース過程を残すため。
SwiftUIがめちゃめちゃ書きやすい
なぜstoryboardからSwiftUIに切り替えたのか
私はアプリ開発全くの初心者なのですが、先月までSwiftのstoryboardを使ってアプリ開発を行っていました。
【storyboard編:一旦頓挫】初心者がApp StoreにiOS自作アプリを公開する過程
【storyboard編:一旦頓挫】初心者が自作iOSアプリ審査に挑戦中SwiftUIの存在は知っていたのですが、「まだ新しい技術だから、フレームワーク依存のエラーが出がち」とか「情報が少ないからstoryboardでの学習がおすすめ」とか「現場ではstoryboardがまだまだ使われてる」等々、ググって仕入れた情報からstoryboardを選択しアプリ開発をしていました。
しかし、protocol?delegate?optional型?何それ的な感じに陥っていましたし、何よりUIを作るときのAuto rayoutがすごくめんどくさかったです。おそらくこれは、独学の初心者には到底扱えない、学習にめちゃめちゃ時間がかかるんだろうなと実感していました。
それでも、とにかくやってみようと手を動かしてアプリを作ってみましたが、Appleさんにこんなクソアプリリースさせねーよと言われてしまい、一から勉強し直そうと考えました。プログラミング言語は進化のスピードが速いということで、書籍より動画の方がより最新の状況を反映しているだろうとの予想で「udemy」でひとつ動画を買って学習していましたが、全体をさらっとみるのには時間がかかりすぎ、あまり僕の学習スタイルにはあっていないとわかりました。元々僕は本をペラペラめくりながら勉強する方が好きなので、書籍での勉強に切り替えようと思いました。
書籍を選ぶ中でやはり気になるのはどれだけ新しいか、最新のxcodeのバージョンに対応しているかというところでした。その基準で本を選んでいるとやはりSwiftUIの書籍が新しく、最新のxcodeのバーションにも対応しているようでしたので、SwiftUIに切り替えました。
買った本たち
・SwiftUI対応 たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 12/iOS 14対応
・詳細! SwiftUI iPhoneアプリ開発入門ノート[2020] iOS 14+Xcode 12対応
・[増補改訂第3版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plusめちゃくちゃ書きやすい
SwiftUI対応 たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 12/iOS 14対応をとりあえず一周(15時間くらいでサラッと)したあとにアプリを作り始めました。そうするとstoryboardよりも体感20倍くらいの速さでUIができて、感動しました。プレービュー機能のおかげでシミュレータを立ち上げる必要なく見た目の変化がわかるのがスピードが上がった要因かと思います。
UIを整えた後に機能を追加していきました。SwiftUI対応 たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 12/iOS 14対応を振り返りながら、詳細! SwiftUI iPhoneアプリ開発入門ノート[2020] iOS 14+Xcode 12対応を参考書がわりにしながら([増補改訂第3版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plusはこれまでほとんど使っていません)、ググりながらやることで2週間程度で当初予定した機能を含むアプリができました。
iOSアプリ審査
審査1回目(2021/3/19)
storyboardでのアプリ制作のときにすでにアプリの審査提出は経験済みなので、今回も同じ手順で行いました。審査提出から1日以内にレビュワーさんからリジェクトの通知が来ました。今回は2つのリジェクト理由がありました。
Guideline 2.1 - Information Needed
最初に審査に提出したアプリの学習画面の右上にスターアイコンを設置していました。これは次のアップデートでお気に入り機能を追加しようと考えていたためです。そこにレビュワーさんの指摘をもらい、「どこでお気に入りリストが見れるの?」と聞かれました。
次のアップデートでお気に入り機能は追加予定なので、スターアイコンは一旦消しますと返信しました。Guideline 2.3.3 - Performance - Accurate Metadata
こちらは審査提出時のスクリーンショットについてiPadサイズのスクリーンショットがiPhoneUIになってるよという指摘でした。そもそも僕のアプリはiPadようには作っていなかったので、最初の審査提出ではなんでもいいのかなと思っていました。指摘もらいましたので、xcodeでシミュレータを起動しiPad用のサイズのスクリーンショットを提出しました。
審査2回目(2021/3/20)
審査通りました!storyboradも含めて4回目でやっと通りました!
まとめ
最初アプリを提出したときに絶望的なリジェクト理由(Minimum Functionality)を提示され、途方に暮れていました。
しかし、一から勉強し直すきっかけにもなり、Swiftの書き方にも慣れることができたので良い経験になったと思います。このアプリについて、今のところ思い付いているやりたいことを以下に書いておきます。
・お気に入り機能追加
・プレイリスト機能追加(イメージはiPhoneのミュージックアプリ)
・例文リクエスト機能(ユーザーが追加して欲しい例文を僕に送ることができる機能)
- 投稿日:2021-03-21T15:53:06+09:00
【Swift】キーボードに合わせてViewをアニメーションさせる
キーボードに合わせてUITextFieldやUIViewをアニメーションさせたい時に、
animate(withDuration:animations:completion:)
でアニメーションさせようと思ったら上手く動かなかったので調べてみました。
UIResponder
を使うと上手くいったので、困っている人は参考にしてみて下さい!キーボードの表示・非表示を検知する
override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) } // Keybordが表示 @objc dynamic func keyboardWillShow(_ notification: NSNotification){ } // Keybordが非表示 @objc dynamic func keyboardWillHide(_ notification: NSNotification){ }キーボードのアニメーションを抽出・アニーメーションを実行
@objc dynamic func keyboardWillShow(_ notification: NSNotification){ // キーボードのdurationを抽出 *1 let durationKey = UIResponder.keyboardAnimationDurationUserInfoKey let duration = notification.userInfo![durationKey] as! Double // キーボードのframeを抽出する *2 let frameKey = UIResponder.keyboardFrameEndUserInfoKey let keyboardFrameValue = notification.userInfo![frameKey] as! NSValue // アニメーション曲線を抽出する *3 let curveKey = UIResponder.keyboardAnimationCurveUserInfoKey let curveValue = notification.userInfo![curveKey] as! Int let curve = UIView.AnimationCurve(rawValue: curveValue)! let animator = UIViewPropertyAnimator(duration: duration, curve: curve) { // ここにアニメーション化したいレイアウト変更を記述する self.view?.layoutIfNeeded() } animator.startAnimation() }*1 キーボードのduration
*2 キーボードのframe
*3 アニメーション曲線レイアウト設定時の注意
textFieldなどをキーボードの上にくっつけて表示したい場合はレイアウトの設定を
Safe Area
に合わせるとSafe Area
分の空白はキーボードとViewの間にできてしまうので注意です。
こんな感じでSuper View
に合わせましょう!
これをUIViewControlleなどで
extension
して共通で使えるようにするのがおすすめです。
- 投稿日:2021-03-21T11:09:11+09:00
TCAでもUseCase/Repository/DataSourceが使いたい
はじめに
この内容は『iOSアプリ開発のためのFunctional Architecture情報共有会4』のための資料です。
導入
The Composable Architecture (TCA)では副作用実行にグローバルな関数1やClient、またはManagerというのが使われる。公式サンプルだけでなくpointfreeのゲームアプリのコードでもClientがよく出てくる。これを普段よく使われるUseCase/Repository/DataSourceを使いたいというのが発表の主旨。
TCAの副作用実行のための関数やClientについてのおさらい
(グローバルな)関数?
サンプルではグローバルな関数を使っていた。
次のコードはWeb API呼び出しで任意の数字からその数字にまつわるトリビアを返す非同期な処理。
func liveNumberFact(for n: Int) -> Effect<String, NumbersApiError> { return URLSession.shared.dataTaskPublisher(for: URL(string: "http://numbersapi.com/\(n)/trivia")!) .map { data, _ in String(decoding: data, as: UTF8.self) } .catch { _ in // Sometimes numbersapi.com can be flakey, so if it ever fails we will just // default to a mock response. Just("\(n) is a good number Brent") .delay(for: 1, scheduler: DispatchQueue.main) } .setFailureType(to: NumbersApiError.self) .eraseToEffect() }
関数利用側でEnvironmentによってDIする場合。Swiftにおいても、関数も変数として扱えるのでDIするときは
(Parameter) -> Effect
クロージャさえ合えばいい。struct EffectsBasicsEnvironment { var numberFact: (Int) -> Effect<String, NumbersApiError> }これで結果を置き換えられるのでXcodeプレビューでも楽だし、テストコードも楽に書ける。
ただ、グローバルな関数として用意するのは複数人でアプリを作るときはあまりやりたくない。サンプルだから良いけども。
Client/Managerとは?
公式のサンプルではClient/Managerは次の通り。
- Client系
- WebSocketClient
- SpeechClient
- DownloadClient
- AudioPlayerClient
- WeatherClient
- AudioRecorderClient
- Manager系
- LocationManager(これはサンプルというよりTCA公式のLocationラッパーかも)
- MotionManager(これはサンプルというよりTCA公式のMotionラッパーかも)
ちなみにClientってどんなものなんだろう?
Clientには
var
クロージャが処理を返しロジックが入れ替えられるようになってる。struct WeatherClient { var searchLocation: (String) -> Effect<[Location], Failure> var weather: (Int) -> Effect<LocationWeather, Failure> struct Failure: Error, Equatable {} }struct SearchEnvironment { var weatherClient: WeatherClient var mainQueue: AnySchedulerOf<DispatchQueue> }クロージャを変数で入れる場合にEnvironmentに直接入れるのではなく、型としてまとめている。
プロダクション用の実処理自体はextensionに
.live
定数で用意extension WeatherClient { static let live = WeatherClient( // 初期化してクロージャに処理を用意してそれを定数 .live として返す searchLocation: { query in var components = URLComponents(string: "https://www.metaweather.com/api/location/search")! components.queryItems = [URLQueryItem(name: "query", value: query)] return URLSession.shared.dataTaskPublisher(for: components.url!) // URLSessionをDIしてない(する気がない .map { data, _ in data } .decode(type: [Location].self, decoder: jsonDecoder) .mapError { _ in Failure() } .eraseToEffect() }, ...
短所
- 処理がstaticな定数だから変数を参照することはできない
- URLSessionをDIすることができない
- WeatherClient自体を置き換えやすいが、WeatherClient自体をテストする気がない
- 例えばDBで特定の要素が上書きされるロジックのテストとかはやる気がない
おそらくReducerの仕様さえテストできてれば良くて、そこから奥にあるミスっていうのはReducerの仕様に関するテストコードで気付こうという感じなのではないか。
短所を解決するには?
呼び出し時に引数で置き換えたいものを渡す
// Reducer内 case let .locationTapped(location): struct SearchWeatherId: Hashable {} state.locationWeatherRequestInFlight = location return environment.weatherClient .weather(location.id, /* ここでURLSessionを渡す */)もしくは定数
.live
のなかでFactory.getURLSession()
みたいなのを用意してプリプロセッサ的なマクロで切り替えて取り出してもいい(が、その仕組みを自動化せず作るのは自分はあんまり...)。
ここまでのまとめ
- pointfreeの人たちは細かな副作用の単体テストをReducerのテストでカバーしてる
- 副作用での細かな処理のテストをやるには引数で渡すとか、Factory的なのを内部で切り替える
おまけ: 最近公開されたpointfreeのゲームのコードもClient
- DictionaryClient
- FeedbackGeneratorClient
- FileClient
- LocalDatabaseClient
- LowPowerModeClient
- RemoteNotificationsClient
- ServerConfigClient
- UIApplicationClient
- UserDefaultsClient
Managerはない。
UseCase/Repository/DataSourceで副作用を呼び出す
Google I/O 2019 - 2021 Android Appから
層の解説
- ドメイン層(いわくlightweight domain layer)
- データレイヤーとプレゼンテーションレイヤーの間に位置
- UIスレッドから離れた場所でビジネスロジックの個別部分を処理
- UseCaseクラスを中心に構成されてる
- コールバック地獄を避けるために、LiveData を使用してユースケースの結果を公開
- 2020あたりではコルーチンも使われはじめる
- 2018あたりまではexecuteNowメソッドで同期的なものは直接返したり
- そもそもAndroid Blue Printという設計のお手本リポジトリがあった
- データ層
- リポジトリモジュールは、すべてのデータ操作を処理し、アプリの他の部分からデータソースを抽象化する役割を担う
データ層についての引用
私たちはFirestoreを気に入って使用していましたが、将来的に別のデータソースに交換したくなった場合、このアーキテクチャによってすっきりとした方法で交換することができます)
Google I/O 2019 - 2021 Android AppにおけるUseCase実装
... /** * A [UseCase] that returns the [UserSession]s for a user. */ class LoadUserSessionOneShotUseCase @Inject constructor( private val userEventRepository: DefaultSessionAndUserEventRepository, @IoDispatcher dispatcher: CoroutineDispatcher ) : UseCase<Pair<String, SessionId>, UserSession>(dispatcher) { // suspendさせてコルーチンなメソッドだが override suspend fun execute(parameters: Pair<String, SessionId>): UserSession { val (userId, eventId) = parameters // 実処理は非同期ではなく同期処理 return userEventRepository.getUserSession(userId, eventId) } }
- セッション情報を取得するUseCase
- コルーチンで結果
UserSession
を返してる- 処理は
userEventRepository
のgetUserSession
メソッド
- 同期も非同期もインタフェース揃えるため
suspend fun
でコルーチンだけど、同期的に処理2
SwiftでUseCaseを作る場合を考える
- 同期と非同期二つにUseCaseを分ける(まだコルーチンないし)
- Swiftではassociated typeを持つprotocolを作る
- これを解決する型消去のやり方は複数ある
同期処理するSyncUseCaseの例
public protocol SyncUseCase where Failure: Error { associatedtype Parameters associatedtype Success associatedtype Failure func perform(_ parameters: Parameters) -> Result<Success, Failure> }このSyncUseCaseを直接利用する型でそのまま準拠してもそのまま使うことはできないので型消去するがその話は別に書いてる。
ProfileをSaveするUseCaseを例に
public class SaveProfileUseCase: SyncUseCase { public typealias Parameters = Profile public typealias Success = () public typealias Failure = Error private let dataSource: ProfileLocalDataSource init(dataSource: ProfileLocalDataSource) { self.dataSource = dataSource // initでdataSourceを入れ替えられるように } public func perform(_ parameters: Parameters) -> Result<Success, Failure> { dataSource.save(Profiles: parameters) // 実処理はdataSourceのCore Dataとかでやってる } } import ComposableArchitecture extension SaveProfileListUseCase { static func executeEffect(Profile: Parameters) -> Effect<Success, Failure> { .result { perform(profile) } // Effectにして結果を返す } }
UseCase型の長所と短所
- 長所
- initでDataSourceを入れるので実行時に引数で用意するわけじゃない
- 従来のプログラミングスタイルと同じ感じでできる
- UseCase自体のテストコードを書ける(依存してるDataSourceを置き換えられるし)
- 短所
- 関数型のやり方に従来のオブジェクト指向型のプログラミングスタイルが混ざることの心理的抵抗感
- UseCase型がめちゃくちゃ増える
UseCaseは使わないがRepository/DataSourceを使う
UseCase型を使わずに、しかしReducer以外の副作用の単体テストしたいのでRepository/DataSourceは使う場合を考える。
// プロフィールを変更して上書きする的なState, Action Environmentの集まり enum ProfileSettingCore { enum State { ... } struct Action { ... } struct Environment { // DIできるようにする var save: (Profile, ProfileDataSource) -> Result<(), Error> } static let reducer = ... }
ProfileをSaveする例
// プロフィールを変更して上書きする的なState, Action Environmentの集まり enum ProfileSettingCore { enum State { ... } struct Action { ... } struct Environment { // DIできるようにする var save: (Profile, ProfileDataSource) -> Result<(), Error> } static let reducer = ... } extension ProfileSettingCore { enum SideEffect { static func save(profile: Profile, dataSource: ProfileDataSource) -> Result<(), Error> { dataSource.update(profile) // 実質的なUseCaseの処理 } } }
UseCaseを使わないSideEffectでグルーピングされたstatic関数の長所と短所
- 長所
- シンプル(UseCase型の面倒な型消去なんて考えなくていい)
- SideEffect自体のテストコードを書ける(依存してるDataSourceを置き換えられるし)
- 短所
- 副作用実行の引数で依存するRepository/DataSourceを入れるのはかったるい
まとめ
- 何がベストなのかはチームにあってるやり方を採用したらいい
- pointfree自体のやり方でClientをつくるってのもそれがチームに合っていればそれでいい
- 投稿日:2021-03-21T08:08:40+09:00
RxSwiftを使ってMapKitで現在地から場所をサジェストするauto completeを実装するサンプル
現在地付近のの施設を検索できるサンプルになります
MapKitを使用して現在地を取得しながら、UISearchBarに入力した結果をtableViewに返すサンプルになります。
実際に使用する際は、現在地の取得の許可を得るために
info.plistに
Privacy - Location When In Use Usage Description
Privacy - Location Always and When In Use Usage Description
をかいてください。現在地の取得が、
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
常にアップデートされるため、**メートル以内の変更は通知しないよう以下の関数を定義してあります。
func distinctUntilChangeGreaterThan(meters: CLLocationDistance) -> Observable<CLLocation>
渡ってきたCLLocationを
.distinctUntilChangeGreaterThan(meters: CLLocationDistance(1000))
1000メートル以内の変化は通知しないように。var myLocationRelay = BehaviorRelay<CLLocation?>(value: nil) myLocationRelay .compactMap { $0 } .distinctUntilChangeGreaterThan(meters: CLLocationDistance(1000)) .subscribe(onNext: { [weak self] coordinate in let span = MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001) let region = MKCoordinateRegion(center: coordinate.coordinate, span: span) self?.searchCompleter.region = region }) .disposed(by: bag)すべてのコード
import CoreLocation import MapKit import RxCocoa import RxSwift import UIKit class FindLocationViewController: UIViewController { var viewModel: FindLocationViewModel! var bag = DisposeBag() var searchCompleter = MKLocalSearchCompleter() var locationManager: CLLocationManager? var searchResults = [MKLocalSearchCompletion]() var myLocationRelay = BehaviorRelay<CLLocation?>(value: nil) @IBOutlet private weak var searchResultsTable: UITableView! @IBOutlet weak var searchBar: UISearchBar! override func viewDidLoad() { super.viewDidLoad() initialSettings() viewModel.setUp() setBind() whereIsMyLocation() } private func initialSettings() { searchBar.delegate = self searchResultsTable.delegate = self searchResultsTable.dataSource = self searchCompleter.delegate = self let categories: [MKPointOfInterestCategory] = [.fitnessCenter] let filters = MKPointOfInterestFilter(including: categories) searchCompleter.pointOfInterestFilter = .some(filters) } private func setBind() { myLocationRelay .compactMap { $0 } .distinctUntilChangeGreaterThan(meters: CLLocationDistance(1000)) .subscribe(onNext: { [weak self] coordinate in let span = MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001) let region = MKCoordinateRegion(center: coordinate.coordinate, span: span) self?.searchCompleter.region = region }) .disposed(by: bag) } private func whereIsMyLocation() { locationManager = CLLocationManager() locationManager?.delegate = self locationManager?.requestWhenInUseAuthorization() if CLLocationManager.locationServicesEnabled() { locationManager!.startUpdatingLocation() } } } extension FindLocationViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let newLocation = locations.last else { return } let location: CLLocationCoordinate2D = CLLocationCoordinate2DMake(newLocation.coordinate.latitude, newLocation.coordinate.longitude) let formatter = DateFormatter() formatter.timeZone = TimeZone(identifier: "Asia/Tokyo") formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" let date = formatter.string(from: newLocation.timestamp) print("緯度:", location.latitude, "経度:", location.longitude, "時間:", date) let ccLoctaion = CLLocation(latitude: location.latitude, longitude: location.longitude) myLocationRelay.accept(ccLoctaion) } } extension FindLocationViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) let result = searchResults[indexPath.row] let searchRequest = MKLocalSearch.Request(completion: result) let search = MKLocalSearch(request: searchRequest) search.start { response, _ in guard let coordinate = response?.mapItems[0].placemark.coordinate else { return } guard let name = response?.mapItems[0].name else { return } let lat = coordinate.latitude let lon = coordinate.longitude print(lat) print(lon) print(name) let preVC = self.presentingViewController as! WhichGymViewController let gymInfo = GymInfo(coodinate: coordinate, name: name) preVC.viewModel.selectedGymRelay.accept(gymInfo) self.dismiss(animated: true, completion: nil) } } func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return searchResults.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let searchResult = searchResults[indexPath.row] let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil) cell.textLabel?.text = searchResult.title cell.detailTextLabel?.text = searchResult.subtitle return cell } } extension FindLocationViewController: MKLocalSearchCompleterDelegate & UISearchBarDelegate { func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { searchCompleter.queryFragment = searchText } func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { searchResults = completer.results searchResultsTable.reloadData() } func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { // Error } func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool { return true } }import CoreLocation import Foundation import RxSwift extension CLLocationCoordinate2D: Equatable {} public func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude } extension ObservableType where Element == CLLocation { func distinctUntilChangeGreaterThan(meters: CLLocationDistance) -> Observable<CLLocation> { return scan(CLLocation(), accumulator: { lastLocation, location in if lastLocation.distance(from: location) > meters { return location } else { return lastLocation } }).distinctUntilChanged { lhs, rhs in lhs.coordinate == rhs.coordinate } } }
- 投稿日:2021-03-21T07:59:55+09:00
Swift初心者がアプリを作れるようになるまで①
開発環境
MacBook Air (2020)
macOS Big Sur Ver11.2.2
Xcode Ver12.4(12D4e)Swiftってなに?
Swiftはappleが開発したオープンソースの言語で、iOS、Mac、Apple TV、Apple Watch向けアプリの開発に用います。
Swift。誰もが圧倒的に優れたアプリを作れる、パワフルなオープンソースの言語です。
iOS、Mac、Apple TV、Apple Watch向けのアプリを開発するためにAppleが作った、
強固で直感的なプログラミング言語。それがSwiftです。デベロッパのみなさんに、かつてないほどの
自由を届けられるように設計されています。Swiftは簡単に使えて、しかもオープンソースなので、
アイデアがある人なら誰でも、画期的なアプリを作ることができます。
(apple公式HPより引用)おおまかに言えばiPhoneのアプリ開発ができる言語ということですね。
iPhoneアプリの開発はかつてはObject-Cがメインでしたが、Swift及びXcode(Swiftを扱えるエディタ)がリリースされてからはSwiftがメインとなっています。
理由としてはSwiftで作ったアプリは、一般的な検索アルゴリズムがこれまでよりも大幅に速く実行できるからなんです。その速さはなんとObject-Cの最大2.6倍、Python2.7の最大8.4倍と驚きの速度。(apple公式HPより引用)
ちなみにSwiftに日本語版は無いので、英語が苦手な方は翻訳を駆使して頑張ってください。
Swiftを扱うのに必要なもの
Swiftでアプリを制作するには、以下の環境が必要となってきます。
・Mac
・Xcode
・AppleID
そう、これだけです。厳密にはリリースに際してAppleディベロッパプログラムへの登録などが必要になるのですが、今回のテーマは「Swift初心者がアプリを作れるようになるまで」ということで、リリース方法はまた別で紹介しようと思います。ひとまず作るだけならMacとXcodeがあればOKです。(Xcodeが無い方はこちらからインストールしてください)Windowsで開発できないの?
気になりますよね、これ。結論から言うと...可能です。ですが2021/3/6現在Swiftパッケージマネージャと、REPLやデバッグエクスペリエンスに使用するlldbの開発が完了しておらず、とりあえずはなにも考えずにMacでの開発をおすすめします。
どんどん情報は更新されているので、気になる方は「Swift Windows」で検索してみてください。Xcodeの設定を知ろう
Xcodeを開くとこのような画面が出てきます。それぞれの説明をすると、
項目 説明 Create a new Xcode project 新規プロジェクト作成 Clone an existing project 既存プロジェクトのクローン Open a project or file 既存プロジェクトのオープン ...そのまんまですね。
ちなみに下のチェックボックス「Show this window when Xcode launches」のチェックを外すと次回からこのメニュー画面が表示されなくなるので、ひとまず触らなくてOkです。今回は新規作成なのでもちろん「Create a new Xcode project」(赤枠)を選択します。
するとこちらの画面になるはずなので、iOSタブの「App」を選択します。
他にもたくさんアイコンが並んでいると思いますが、これらは最初に組まれる雛形が違うだけでいくらでもカスタマイズができるので、あまり難しく考えずに自身が作りたいものに最も近いものを選べばいいと思います。
今回はひとまず「App」で進めていきます。
すると次はこちらの画面になるので、「Product Name」に今回のプロダクト名を入力します。今回はとりあえず「Sample」としておきます。
以下、他の項目については以下のような感じです。
項目 説明 Product Name プロダクト名、アプリ名 Team プロダクトを作成する団体名(もしくは個人名) Organization Identifier プロダクトを作成する団体のID(もしくは個人) Bundle Identifier 今回のID(Organization Identifier + . + Product Name) Interface 実装方法の設定(SwiftUI, StoryBoardから選択) Life Cycle ライフサイクルの選択 Languages 使用するプログラミング言語(Swift、 Objective-Cから選択) 上半分に関しては自身で設定すれば良いのですが、下部に関しては少し知識が必要かもしれません。
InterFaceについて
SwiftのインターフェイスはSwiftUI, StoryBoardの2つとなっていますが、これらの違いはコードでUIを実装するか、GUIでUIを実装するか。といったところです。
以前まではStoryBoard, Xib, UIKitの3つが存在していたのですが、iOS13にてSwiftUIが追加されました。
どれを選択するかは一概には言えず、アプリの特徴や開発者の慣れなどで考えるのがいいと思います。
ひとまず今回はサンプルを動かすだけなので、何も考えずにSwiftUIを選択します。Life Cycleについて
Life Cycle(ライフサイクル)ってなんだ...わかります。その気持ち。知ってた方は飛ばしてちゃってください。
私もSwift学習を開始してから知ったので、知らなくても全く問題ないです。
ライフサイクルはアプリ内の状態遷移を表す言葉です。...どゆこと。
実際にiPhoneでアプリを使用するときを想像してみてください。
1.アイコンをタップする
2.画面が起動画面に切り替わる
3.画面がアプリ画面に切り替わる
4.画面のみ閉じるとアプリがバックグラウンドへ
5.再度起動すると前回使用画面から起動
6.タスクキルでアプリ終了
こういった一連の流れでアプリ側が保持している状態をライフサイクルといいます。
だいぶ説明を省いているので詳しく知りたい方は「アプリ ライフサイクル」で検索してみてください。今回は「SwiftUI App」でOKです。
Languageについて
SwiftとObjectiv-Cからの選択です。(上記InterFaceでSwiftUIを選択している場合Swiftのみ選択可能)
こちらは使用言語についてです。
SwiftとObjective-Cの選択に関しては、一概には言えませんがSwiftの方が新しいことや、Swiftの方がAppleがSwiftを推奨していることからざっくりとSwiftの方が良いとされています。
ちゃんとした理由を知りたい方はご自身で調べることをおすすめします。下部の選択項目について
上記の項目の下に写真のような選択項目があるのですが、これらに関しては一応軽い説明を載せますが、今回は全て選択しなくて大丈夫です。
Use Core Data / Host in CloudKit
「Use Core Data」には名前の通りCore Dataというものを使用するかどうかの選択です。
Core DataというのはざっくりXcode上からデータベース構造の設定を行えたり、使用するデータの保存、削除、更新を行うプログラムを書くための仕組みのことです。
こちらに関しても詳細はご自身で調べてください。(ちゃんと説明するとそれだけで1記事分になってしまうので...)
「Host in CloudKit」に関してですが、これはCloudKitを理解することから始まります。
簡単に言えばiCloudにデータを保存できるサービスです。AppleサポートのCloudKitのページを見てみると、CloudKitを使用するとキー値データ、構造化データ、および各種アセットをiCloudに保存できるようです。そして公開データベースと非公開データベースの両方に対応しているので、プライベートなデータ、パブリックなデータ両方扱えることになります。例えばAppleIDを使用してプライベートなデータを管理することも可能です。これはiOS標準アプリの「メモ」などにも使用されており、iPhoneでメモに何か保存すると、AppleIDで同期されているmacなどのメモにも表示されるようになります。これはCloudKitによって管理されていたんです。Include Tests
続いて「Include Tests」についてです。これに関しては単純で、チェックを入れることでテスト用のターゲットとテスト用のコードが生成されます。なのでとりあえずはチェックを入れないで大丈夫です。
これですべての設定を終えたので、右下のNextボタンを押して次に進みます。
まとめ
ひとまず今回はここまでです。
xCodeは日本語版が無いということで設定内容を理解することだけで一苦労ですね...。
次回からはサンプルコードを使用してエディター内の項目解説に移るので、更新しましたらぜひ「Swift初心者がアプリを作れるようになるまで②」もご覧ください。最後までお読みいただきありがとうございました!
- 投稿日:2021-03-21T02:46:45+09:00
xcodeでデバッグするためのプロビジョニングプロファイル作成
概要
<xcode経由でデバッグ>
方法1:
Automatically manage signing
をチェックすると
AdHocのプロビジョニングプロファイルはなくても、Xcodeで
自動で選択して、なければ自動生成できる。
https://qiita.com/rotors123/items/a015ff0c3c3ed3cf5c47方法2:
Automatically manage signingの
チェックを入れずに以下を実施する
証明書要求ファイルを作る(以前作ったものがあればそれを使える)
Development証明書を作る
Developmentプロビジョニングを作る。
iPhone端末に上記をインストールする
ビルドターゲットにそのプロビジョニングプロファイルを指定してビルド<ipaでインストール>
証明書要求ファイルを作る(以前作ったものがあればそれを使える)
Distribution証明書を作る(Store用とAdHocは同じものを使える)
DistributionのAdhocプロビジョニングを作る
ビルドターゲットにそのプロビジョニングプロファイルを指定
アーカイブでipaを出力
※デバイス追加の都度、UUID登録、Provisioning Profile更新、リビルド等の作業が必要<store公開>
証明書要求ファイルを作る(以前作ったものがあればそれを使える)
Distribution証明書を作る(Store用とAdHocは同じものを使える)
DistributionのStoreプロビジョニングを作る
ビルドターゲットにそのプロビジョニングプロファイルを指定
アーカイブでipaを出力<補足>
参考手順
https://hirokuma.blog/?p=2783プロビジョニングプロファイルの有効期限(1年)が過ぎた場合は更新が必要になる
https://qiita.com/Labi/items/d93624e4f6ec7d073e76build schme(起動時に端末と一緒に選択する実行環境 = 以下をまとめたもの)
・build target(ビルド情報:プロビジョニングプロファイルとかを設定)
・build configuration(コード上でdebug/releaseを分けたいときに利用)
->build targetを追加すると、自動でbuild schmeも追加される
https://qiita.com/kazy_dev/items/feb68f162ec3d91005d3xcodeでデバッグするためのプロビジョニングプロファイル作成
以下を参考に、証明書要求ファイルを作っておく
https://i-app-tec.com/ios/apply-application.html↓以下の「+」を押下
↓ iOS App Developmentを選択
※iOS Distribution (App Store and Ad Hoc)のApp Storeはストア公開用、Ad Hocはipaでのインストール用↓証明書要求ファイルを選択
↓ダウンロード(ios_development.cer)
↓ダウンロードできたらダブルクリックしてキーチェーンに登録↓+をクリック
↓
↓Apple IDを選択
↓
↓上記Create Devicesで、一旦端末登録画面に飛ばされる
XcodeのDevvicesのSerial Number: の下の IdentifierがUUID↓
↓
↓
↓わかりやすい名前をつけてダウンロード
↓
↓+を選んで、さっきのプロファイルを選んでinstall
最後に、signingのprovisioning Profileに先程のプロビジョニングプロファイルを設定すると、実機デバッグが可能になる
※ビルドスキームを追加すると、実行時のスキームを切り替えるとプロビジョニングプロファイルを切り替えれるので便利。以下手順参考
https://qiita.com/kazy_dev/items/feb68f162ec3d91005d3補足1 ipaでデバッグするためのプロビジョニングプロファイル作成
Generate a Provisioning Profileページ
->AdhocはDistribution用の証明書を使うため、ストア公開用の証明書を使えるので、それを選択作成されたらプロビジョニングプロファイルをダウンロードし、ビルドターゲットに指定
端末はAny iOS Deviceを選択し、上記のビルドターゲットを指定してProduct -> Archive
Validate Appはせずに
Distribute App->Ad Hoc->プロビジョニングプロファイルはAd Hoc用を選択。
Distribution CertificateはDistribution用の証明書を使うため、ストア公開用の証明書を使えるので、それを選択
ipaがExportされるので、Apple Configurator 2などでインストールする参考
https://makotton.com/2015/06/23/1063補足2 Testflightの内部テスト
Appstore Connectで
ユーザーとアクセスを選択し、画面に従ってユーザーを登録
ユーザーに招待メールが届くので、それに同意してもらう
App Store Connectをユーザ選択 -> ユーザーを登録すると、招待メールが届く
ユーザーは招待メールにアクセスし、Testflightアプリをインストールすると、Testflight経由でテストができる
(この方法は「内部テスト」という仕組みで、審査不要。「外部テスト」というのはテスト用の審査が必要)
https://qiita.com/haseken_dev/items/8c094b44c7e67836b74d
https://qiita.com/john-rocky/items/9e3538bfec0300cdc7ea補足3 Store公開用のプロビジョニングプロファイル作成参考サイト
https://www.small.co.jp/application-develop/ios-%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92-adhoc-%E3%81%A7%E5%85%AC%E9%96%8B%E3%81%99%E3%82%8B/
Ad Hocを後悔するのはこういう方法もあるみたい公開用のwebサイトを用意する
ipaファイル・plistファイルを同じディレクトリに配置する
webページにplistファイルを開くリンクを設ける(これをiosのブラウザでクリックするとアプリのインストールが実行される)
※plistファイルを編集することでipaファイルのパスを任意に指定すること等も可能
- 投稿日:2021-03-21T01:42:47+09:00
SwiftUIでフルスクリーンのモーダルを表示する(iOS13)
SwiftUI(iOS13)でフルスクリーンモーダルを表示する
久しぶりの記事になります。おはこんばんにちは和尚です!
今日は一昨年Appleから発表されて話題となっているSwiftUIについての記事を書いていきたいと思いますSwiftUI 2はiOS13では使用できない件
現在(2021.3月時点)ではSwiftUIはver.2までリリースされておりますが、残念なことにSwiftUI 2でリリースされた機能はiOS13で使用することができません。SwiftUI 1では一般的なアプリに必要な機能が全然揃っておらず、2以上にライブラリやUIKitに頼ることになります。
必須級ライブラリ
SwiftUIX
↑ SwiftUIにまだ実装されていないものを補ってくれます。説明書がないものがチラホラあるのが残念...さて、本題であるフルスクリーンのモーダルですが、公式からは「.fullScreenCover」というものが用意されていますがこちらはSwiftUI 2から登場したものとなっており残念ながらiOS13では使用することができません。
ので、今回iOS13でも使えるフルスクリーンのモーダルを作っていきましょう!※ちなみに筆者、初めてのiOSアプリ実装がSwiftUIとなっておりUIKitも現在絶賛勉強中ですのでお手柔らかに(笑)
フルスクリーンのモーダル実装
1. UIApplicationの拡張
UIApplicationを拡張して一番上のコントローラーを取得するメソッドと、フルスクリーンのモーダルを閉じるメソッドを作成していきます。
UIApplication+Extension.swiftextension UIApplication { /// 一番上にあるコントローラーを取得する public func getTopViewController() -> UIViewController? { guard let window = UIApplication.shared .connectedScenes .filter({$0.activationState == .foregroundActive}) .map({$0 as? UIWindowScene}) .compactMap({$0}) .first?.windows.first else { return nil } window.makeKeyAndVisible() guard let rootViewController = window.rootViewController else { return nil } var topController = rootViewController while let newTopController = topController.presentedViewController { topController = newTopController } return topController } /// フルスクリーンのモーダルを閉じる public func closeModalView() { UIApplication.shared.getTopViewController()?.dismiss(animated: true, completion: nil) } }2. Viewの拡張
次はSwiftUIのViewでSheetViewやFullCreenCoverのようにフルスクリーンのモーダルが使用できるようにViewを拡張していきます。
View+Extension.swiftextension View { public func fullScreenView<Content>( isPresented: Binding<Bool>, @ViewBuilder content: @escaping () -> Content ) -> some View where Content: View { if isPresented.wrappedValue { let window = UIApplication.shared.windows.last window?.isHidden = true let view = content() let viewController = UIHostingController(rootView: view) viewController.modalPresentationStyle = .fullScreen DispatchQueue.main.async { guard let tvc = UIApplication.shared.getTopViewController() else { return } tvc.present(viewController, animated: true, completion: nil) isPresented.wrappedValue = false } } return self } }実際に使ってみよう!
実際にViewで使用してみましょう!今回適当なサンプルViewを用意してみました!
こちら参考にみなさん是非使ってみてくださいContentView.swiftstruct ContentView: View { @State private var showModal: Bool = false var body: some View { VStack { Spacer() Button(action: { showModal.toggle() }, label: { Text("フルスクリーンのモーダルを表示する") }) Spacer() } .fullScreenView(isPresented: $showModal) { ModalView() } } } struct ModalView: View { var body: some View { ZStack { Color.green.edgesIgnoringSafeArea(.all) Button(action: { UIApplication.shared.closeModalView() }, label: { Text("閉じる") }) } } }最後に
iOS13でSwiftUIとCombineを使用する際は、特にiOS13.0からiOS13.2までは気をつけましょう。バグがかなり多いです...
しかも、たちが悪いことにシュミレーションでは再現せず実機のみで再現するバグもいくつかあります。実機がない場合は、SwiftUIをiOS13で使用することをおすすめしません
それでもiOS13を含めたい場合は最低でもiOS13.3以上にしましょう。Let's Enjoy SwiftUI!!
- 投稿日:2021-03-21T01:06:29+09:00
【Swift】IntにSetAlgebraを実装して集合のように扱えるようにする
Intのビットを使って集合演算をするとき、ビット演算をSetAlgebraで覆ってあげたほうが使いやすいかもと思って書いてみた。
関数を一つでも削るとコンパイルが通らないような極小の実装になっている。
let a: Int = [0, 2, 3] let b: Int = [0, 2, 4] assert(a.union(b) == 1 + 4 + 8 + 16) assert(a.union(b) == [0, 2, 3, 4]) assert(a.intersection(b) == [0, 2]) assert(a.symmetricDifference(b) == [3, 4]) assert(a.isSubset(of: [0, 1, 2, 3])) extension Int: SetAlgebra { public typealias Element = Int public func contains(_ member: Element) -> Bool { self >> member & 1 == 1 } public func intersection(_ other: Self) -> Self { self & other } public func union(_ other: Self) -> Self { self | other } public func symmetricDifference(_ other: Self) -> Self { self ^ other } public mutating func formIntersection(_ other: Self) { self &= other } public mutating func formUnion(_ other: Self) { self |= other } public mutating func formSymmetricDifference(_ other: Self) { self ^= other } public mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element) { if contains(newMember) { return (false, newMember) } else { self |= 1 << newMember return (true, newMember) } } public mutating func remove(_ member: Element) -> Element? { if contains(member) { self &= ~(1 << member) return member } else { return nil } } public mutating func update(with newMember: Element) -> Element? { if contains(newMember) { return newMember } else { self |= 1 << newMember return nil } } }