- 投稿日:2021-06-13T23:33:21+09:00
The Composable Architecture(TCA)の紹介と少し使った所感
概要 The Composable Architectureという良さげなアーキテクチャがあると聞いて、iOSの簡単なアプリをSwiftUIで書いてみました。 作ったもの https://github.com/tonionagauzzi/SwiftUITCASample よくチュートリアルで作るTODOアプリです。 実は、Qiitaにほぼ同じことをされた先人さんがいました…(書く段階で知った。笑) オリジナル性は無いですが、社内勉強会向けの資料なので、ご容赦ください! The Composable Architectureの説明 全体図 出典:The Composable Architecture — Visualize Data Flows With a Diagram 各コンポーネントの説明をしていきます。 State Action Environment Reducer Store 先ほどの図でState、Action、Reducer、Effectを囲んでいるのがStoreです。 Effect State UIに表示する内容を定義します。 ContentView.swift // ToDo1個分のState。 struct ToDoState: Equatable, Identifiable { let id: UUID var description = "" var isCompleted = false } // アプリ全体のState。ToDoの配列。 struct AppState: Equatable { var todoStates: [ToDoState] = [] } たとえば1つの画面でこれらを更新すると、他の画面にも即反映されます。 Action タップやデータ受信などのイベントを定義します。 ContentView.swift // ToDo1個に対して発生するイベント。 enum ToDoAction: Equatable { case checkTapped case textChanged(String) case removed } // アプリ全体に対して発生するイベント。TODOのは何番目に何を送るかを指定。 enum AppAction: Equatable { case todo(index: Int, action: ToDoAction) case addButtonTapped } Stateもそうですが、Equatableに準拠することでテストが容易になります(後述)。 Environment 依存関係を外部から注入します。DIです。今回はあまり活用しませんでした。 ContentView.swift struct ToDoEnvironment { } struct AppEnvironment { var uuid: () -> UUID = UUID.init } DIとはなんぞやについては別記事を参照。Androidの記事ですが。 Reducer Actionを受けてStateを更新する役割です。 ContentView.swift let todoReducer = Reducer<ToDoState, ToDoAction, ToDoEnvironment> { state, action, environment in switch action { case .checkTapped: state.isCompleted.toggle() return .none case .textChanged(let text): state.description = text return .none case .removed: return .none } } let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine( todoReducer.forEach( state: \AppState.todoStates, action: /AppAction.todo(index:action:), environment: { _ in ToDoEnvironment() } ), Reducer { state, action, environment in switch action { case .todo(index: _, action: .checkTapped): state.todoStates = state.todoStates .enumerated() .sorted { $0.element.description.lowercased() < $1.element.description.lowercased() } .sorted { !$0.element.isCompleted && $1.element.isCompleted } .map (\.element) return .none case .todo(index: let index, action: .removed): state.todoStates.remove(at: index) return .none case .todo(index: let index, action: let action): return .none case .addButtonTapped: state.todoStates.insert( ToDoState(id: environment.uuid()), at: state.todoStates.count ) return .none } } ) .debug() ユーザー入力も処理結果も含めた全てのアクションがここへやって来ます。 Store(ViewStore) State、Action、そしてReducerの1セットをStoreと呼びます。Viewを構築する役割です。 ContentView.swift let todoStore: Store<ToDoState, ToDoAction> let appStore: Store<AppState, AppAction> Storeは、Stateの変更を全て監視してViewを再レンダリングします。 また、Viewから発生したActionをReducerに渡す役割もあります。 Effect さて、今回は単純なサンプルなので、Reducerでは全部return .noneしていました。 しかし、本来Reducerは必要に応じてEffectを使います。 こちらの例を使って説明します。 struct AppEnvironment { var mainQueue: AnySchedulerOf<DispatchQueue> var numberFact: (Int) -> Effect<String, ApiError> } ↑まず、EnvironmentのnumberFactで時間のかかる処理をクロージャとして注入します。 処理はUIと非同期で実行したいので、実行スレッドの情報をmainQueueで渡します。 let appView = AppView( store: Store( initialState: AppState(), reducer: appReducer, environment: AppEnvironment( mainQueue: .main, numberFact: { number in Effect(value: "\(number) is a good number Brent") } ) ) ) ↑これが渡し元です。アプリ全体のStore初期化時に、依存関係を注入しています。 case .numberFactButtonTapped: return environment.numberFact(state.count) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.numberFactResponse) ↑そしてReducerでは、受け取った処理を行いつつ、非同期に監視します。何かしらreceiveしたら、Reducer自身に対して新たなActionを発行します。 [NOTE] ちなみに、returnの型はEffect<AppAction, Never>型となっています。 case let .numberFactResponse(.success(fact)): state.numberFactAlert = fact return .none case .numberFactResponse(.failure): state.numberFactAlert = "Could not load a number fact :(" return .none ↑Reducerは新たなActionを受け取ったら、stateを更新します。成功/失敗の2種類あります。 このようにして、副作用と呼ばれるものを外部に出さずReducer内で完結させられるのが、Reduxなどには無いThe Composable Architectureの特徴です。 TCAの強み State management Composition Side effects Testing Ergonomics 出典:What is the Composable Architecture? 一言で言えば、画面をまたいで状態を値型で共有でき、小機能単位でStoreを分割でき、Actionの副作用をReducer内部で扱うことができ、ビジネスロジックのテストが容易で、これらすべてを短く人間工学的なコードで書ける、といったところでしょう。 テストのしやすさ ToDoリストに削除機能を追加したコミットです。 テストに注目します。 SwiftUITCASampleTests.swift func testRemoveToDo() { let (uuid1, uuid2) = (UUID.init(), UUID.init()) let store = TestStore( initialState: AppState( todoStates: [ ToDoState( id: uuid1, description: "ToDo 1", isCompleted: false ), ToDoState( id: uuid2, description: "ToDo 2", isCompleted: true ) ] ), reducer: appReducer, environment: AppEnvironment() ) store.assert( .send(.todo(index: 0, action: .removed)) { expected in expected.todoStates = [ ToDoState( id: uuid2, description: "ToDo 2", isCompleted: true ) ] } ) } 初期initialStateと、送るactionとを指定し、実行後のexpectedが期待通りのStateであることを確認するテストですが、非常に読みやすく書けました。 また、サーバーやDBを使い分けるような大きなアプリになったとき、MockEnvironmentみたいなダミーをenvironmentに注入すれば、ビジネスロジックだけをテストすることも簡単です。 補足 Swift UIとCombineに依存しているため、iOS 13、macOS 10.15、Mac Catalyst 13、tvOS 13、および watchOS 6以上でなければ使えませんが、古いOS向けにReactiveSwiftとRxSwiftのForkも一応用意されています。 所感 クリーンアーキテクチャーの記事を書いたのがちょうど2年前です。ってか、The Clean ArchitectureもTCAですね笑 今回のTCAを使ってみて、機能追加のしやすさやモジュール分割の自由度など拡張性の高さを感じました。この勢いで、もう少し複雑なアプリも作ってみたいですね。 一方、現状はApple製の新しめOS向けに特化したアーキテクチャなので、X-Platform前提の時点で難しかったり、古いOSのサポートを切れない実案件では選定しにくいという課題はあります。 実際、大規模案件では今でもMVVMやクリーンなほうのTCAを扱うことが多いです。 が、新しいアーキテクチャがあちこちで使われ出してるなと感じたら、基礎だけでも素早く学ぶよう個人的に心掛けています。 なぜかというと、人の書いたコードを読む抵抗が減るからです。 昨今、1から何かを発明する開発スタイルは淘汰され、既にあるものを繋ぎ合わせてニーズを実現するのが、いろんな分野で主流になって来てると感じます(※諸説あり)。 人の書いたコードやAPI仕様書を見たとき、いちいちアレルギーを発症していたらやっていけないので、「これはこういう思想でこうなんだな」とすぐに見抜くために、自分の中にパターン化された前提知識をいくつも持っていたいと思います。 と、良くない方向にポエム化して来たので、オチをつけましょう。最小機能単位で切り出しと疎結合ができるコンポーザブルアーキテクチャは、まさにこの切り貼りの時代にうってつけな感じですね! 参考記事 ※もっとよく知りたい方は、これらの素晴らしい記事orセッションをおすすめします! GitHub @pointfreeco/swift-composable-architecture Swiftによるアプリ開発のためのThe Composable Architectureがすごく良いので紹介したい the Composable Architecture の始め方 Composable Architecture を利用した Todo アプリの紹介(Part 1) CA.swift #14 - モジュール分割した開発での知見とテーマ切り替えやUICatalogについて
- 投稿日:2021-06-13T20:17:04+09:00
【SwiftUI】Toggleスイッチを非活性にする方法
アプリ作成を進める中で2つToggleスイッチを使用する場面がありました。 どちらか片方が選択されている場合、もう片方は非活性にしたかったのですが、 調べても参考になる記事が出てこなかったのでメモとして残しておきます。 初学者の方に参考になればと思います。 実装方法 とてもシンプルで、buttonと同じくdisabledモディファイアを使用して実現できました。 ソースコード SwiftUI import SwiftUI struct ContentView: View { // トグルスイッチフラグ // デフォルトはoffにしたいためfalse @State private var toggleFlg1 = false @State private var toggleFlg2 = false var body: some View { HStack(spacing: 15) { Text("スイッチ1") .lineLimit(1) Text(toggleFlg1 ? "ON" : "OFF") .font(.custom("Times-Roman", size: 15)) Toggle("", isOn: $toggleFlg1) .disabled(toggleFlg2) .labelsHidden() Text("スイッチ2") .lineLimit(1) .font(.body) Text(toggleFlg2 ? "ON" : "OFF") .font(.custom("Times-Roman", size: 15)) Toggle("", isOn: $toggleFlg2) .disabled(toggleFlg1) .labelsHidden() .font(.body) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
- 投稿日:2021-06-13T17:45:50+09:00
【Swift】@IBInspectableと@IBDesignable
はじめに 今回はIBDesignableとIBInspectableについて解説してみたいと思います! IBInspectable これを使うとストーリーボードのInterfaceBuilderで設定できるようになります。 前回のプロジェクトを使って解説します。 このように、placeHolderに対して@IBInspectableを使います。 @IBInspectable var placeHolder: String = "" { willSet { self.placeHolderLabel.text = newValue self.placeHolderLabel.sizeToFit() } } すると、ストーリーボードのInterfaceBuilderで変更可能になります。 ただし、コードでplaceHolderを指定した場合はそちらが優先されます。 final class ViewController: UIViewController { @IBOutlet private weak var textView: PlaceTextView! override func viewDidLoad() { super.viewDidLoad() textView.placeHolder = "カキクケコ" } } IBDesignable IBInspectableを指定し、リアルタイムで見た目の変更をストーリーボードで見れるようにしてくれます。 つまり、ビルドしなくても確認できるようにするためのものです。 ストーリーボードで適当にUIViewを配置してください。 そして、ViewControllerで以下のように書いてみます。 final class DesignableView: UIView { @IBInspectable var cornerRadius: CGFloat { get { return layer.cornerRadius } set { layer.cornerRadius = newValue } } } 先ほど配置したViewにこのDesignableViewを継承させます。 先ほどの@IBInspectableのおかげでストーリーボードから変更できるようになっているので、適当に値を変えます。 ただし、この時点では@IBDesignableをまだ使っていないのでリアルタイムでは変更されません。 @IBDesignableをつけてあげます。 @IBDesignable final class DesignableView: UIView { @IBInspectable var cornerRadius: CGFloat { get { return layer.cornerRadius } set { layer.cornerRadius = newValue } } } すると、このようにリアルタイムで変更が見られるようになると思います! おわりに @IBInspectableと@IBDesignableは便利ですね!積極的に使っていきたいです!
- 投稿日:2021-06-13T14:39:10+09:00
FontAwesome.swift備忘録
導入 公式を参照のこと アイコンの探し方 上の公式サイト検索欄から検索。 カーソルをかざした際に「Pro」と右上に表示されないアイコンが無料で使える。 クリックするとアイコンの詳細画面が表示されるのでそこからstyleが確認できる。 呼び出し方 探したアイコン名とstyle、あとは色とサイズを指定するだけ。 // インポート import FontAwesome_swift import UIKit // 例 youtubeのアイコン, brandsのスタイルが無料で使える let itemIcon = UIImage.fontAwesomeIcon(name: .youtube, style: .brands, textColor: .gray, size: CGSize(30, 30) 注意点 FontAwesomeのアイコンはProプランに加入しないと扱えないアイコンも多く存在する。 FontAwesome.swiftからデフォルトで呼び出せるのは無料アイコンのみ。(Pro使えるようにする方法もあり) 表示できないアイコンは?アイコンが表示される。 同名のアイコンでも指定したstyleが無料で使えないこともあるので注意。 改善案 アプリ内で独自に使いたいアイコンはFAImageクラスとか別のクラス作成してそこから使い回すと、 毎回どのスタイルが無料で使えるか確認しなくて済みそう。
- 投稿日:2021-06-13T09:58:07+09:00
Flutter と Firebaseで開発者一人でマッチングアプリを開発する
こんにちは!CROSSERの開発者です 2021年4月にAndroid、2021年5月にiOSを語学マッチングアプリ「CROSSER」をリリースしました。 ここではFlutterとFirebaseを用いた開発〜リリース〜運用で参考にした資料、つまづいた点などを共有したいと思います。 アプリ概要 語学学習目的で語学を教えてくれるパートナー探し 世界規模 性別、年齢、国籍、自分からの距離で相手を探せる 収益方法はアイテム内課金、アプリ内広告 iOS、Androidモバイル対応(iPadはこれから) ダウンロードはこちらからお願いします https://instabio.cc/crosser チーム構成 開発者1名で開発しました。 非開発者(1名) 開発者(1名) バックエンド開発経験4年 フロントエンドの開発経験無し 主なアプリ機能 ログイン(電話番号、Facebook、Apple、LINE、Kakao) ログインアカウントをG-mail連携 アプリ内課金(消費型、サブスクリプション型) メッセージ ユーザーブロック ユーザー通報 カードスワイプ(Tinder風) 位置情報検索 一般的なマッチングアプリと同じような機能だと思います。 どうしてFlutterとFirebaseだったか? 開発者が1名、かつ半年以内にiOS版、Android版を同時リリースということが目標でした。(結果的には1年かかりましたが。。) 半年以内にiOS版、Android版をネイティブアプリで開発するのは厳しいなと思い、Cross Platformで開発をすることを考えました。(平日は会社にいるので、開発はほぼ土日しかありませんでした) 最初はMonacaを使ってサーバー側はゴリゴリに構築・開発していました。 。。が!バックエンド開発しかしたことがない者としてバリバリcss, jsを使って開発することがストレスで。。。半年、ずっと戦っていました。。。 そんなある日、会社の先輩がFlutterいいよ!と話があったので、ちょっと触って見たところ、おぉお!これだ!と感動し、ほぼMonacaで完成していたアプリをGitから削除し、Flutterで開発することを決心しました (Javaを触ったことがある方にFlutterは持ってこいです!しかもPackageも沢山あるため、簡単に映えるUIを作れます!) その流れで、リリース後のメンテを現実的に考えてFirebaseを採用しました。(←最初から考えとけよ) Firebaseで使用している機能 どれをどの機能に使ったかは別記事で詳しく説明します Firebaseの各機能についてはこちらの記事が分かりやすいです! https://qiita.com/shibukk/items/4a015c5b3296563ac19d RealTime Database Cloud Firestore Firebase Hosting Analytics Firebase Authentication Cloud Storage for Firebase Firebase Cloud Messaging Firebase In-App Messaging Firebase Remote Config まとめ Flutter、Firebase最高 Google最高 今後はこのページに新規記事のリンクをつけて知見を共有していこうと思いますので、よろしくお願いします。 みなさん、是非、語学マッチングアプリCROSSERのダウンロードお願いします 評価もしていただけると嬉しいです!(高評価だともっと嬉しです!) 追記 関連する追加記事情報を載せます。 - FirebaseでLINE/Kakaoログインする方法https://qiita.com/crosser202008/items/ed240474439aca7f7cec Firebaseでプッシュ通知をする方法 https://qiita.com/crosser202008/items/4074177b69960c67bcf5
- 投稿日:2021-06-13T06:49:52+09:00
UITextViewの選択を無効化しリンクのみタップを可能にする
はじめに どうも@kaneko77です たまーにあるリンク化したテキストを実装する際、 リンク以外もタップできちゃう問題発生する時あると思います。 今回はそちらに焦点を当てて解決策を共有していきたいと思います。 環境 Xcode13 Swift5.5 IOS15 遭遇シーン 以下挙動があると思います。 リンク入れたけど他タップできるやん!!ですね。 それをリンクのタップだけ機能して他はタップできないように実装しました。 コード それではどういう風に書いてどういう風に呼び出すか書いてきます。 大層に呼び出しとか言ってますが、実際にはUITextViewのカスタムクラスを作成して、 そのカスタムクラスを呼び出す形になっています。 呼び出し側 let hogeText: CustomTextView = { let textView = CustomTextView() textView.translatesAutoresizingMaskIntoConstraints = false textView.font = .boldSystemFont(ofSize: 20) textView.textColor = .black textView.textAlignment = .center textView.isSelectable = true textView.isEditable = false textView.isScrollEnabled = false let text = "設定アプリを開きたい場合は⑴こちら\n Googleを表示したい方は⑵こちら" let attributeds = NSMutableAttributedString(string: text) attributeds.addAttribute( .link, value: UIApplication.openSettingsURLString, range: NSString(string: text).range(of: "⑴こちら") ) attributeds.addAttribute( .link, value: "https://www.google.co.jp/", range: NSString(string: text).range(of: "⑵こちら") ) textView.attributedText = attributeds return textView }() 呼び出し元 import UIKit class CustomTextView: UITextView { // MARK: - リンクのみタップ可能にする override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { guard let p = closestPosition(to: point), let range = tokenizer.rangeEnclosingPosition( p, with: .character, inDirection: UITextDirection(rawValue: UITextLayoutDirection.left.rawValue) ) else { return false } let link = attributedText.attribute( .link, at: offset(from: beginningOfDocument, to: range.start), effectiveRange: nil ) return link != nil } // MARK: - テキスト選択時にメニューの表示を禁止する override func becomeFirstResponder() -> Bool { return false } } 終わりに 今回はリンクのみタップ可能になる方法のご紹介でした。 かなり簡単にイライラする要因がなくなるため嬉しいですね。 参考にしたサイト
- 投稿日:2021-06-13T00:49:03+09:00
【SwiftUI】Chartsを使って線グラフを表示する
はじめに 今回はSWiftUIを使って線グラフを表示していきたいと思います。(厳密にはSWiftUIでUIViewを使います) 使用するライブラリはChartsです。 1. 作った物 本記事のコードを用いることでこのようなグラフを表示することができます。 2.ライブラリをインポート CocoaPodからライブラリを導入します。 まずはプロジェクトディレクトリでpod init 続いてPodfileにpod 'Charts'を追記してpod install あとはプロジェクト内でimport Chartsとすれば使えるようになります。 3.SwiftUIでUIViewを使う Chartsライブラリですが、SwiftUIには対応しておらず、従来のUIKitのライブラリとなります。 そのため使うためにはSwiftUIでラップする必要があります。 SwiftUIしか触ったことがない私はこの時点で???でしたが、 調べてみて何となくわかってきました。 UIViewRepresentableプロトコル UIKitのクラスをラップするにはUIViewRepresentableプロトコルを使用します。 ラッパークラス(構造体?)のLineChartを作成します。(ここの名前は自由です) そしてLineChartViewというのが今回ラップ対象のクラスです。 struct LineChart : UIViewRepresentable { func makeUIView(context: Context) -> LineChartView { // ここでViewが作られるイメージ } func updateUIView(_ uiView: LineChartView, context: Context) { // 値が更新された時に呼び出される処理 } } また、UIViewRepresentableプロトコルはmakeUIView関数とupdateUIView関数の実装が必須になります。 makeUIViewで表示するViewが作られるイメージ updateUIViewでは値が更新された時に呼び出される処理を書きます。例えば数値を追加した時にそれを動的にグラフに反映する時に使用します。今回はそこまで実装しません。 実装 こちらが全文となります。makeUIViewの箇所がメインとなります。 dataSetをdataにいれてdataをチャートに反映する。 というだけですのでそこまで複雑な内容ではありません。 import SwiftUI import Charts struct LineChart : UIViewRepresentable { func updateUIView(_ uiView: LineChartView, context: Context) { // update } func makeUIView(context: Context) -> LineChartView { // グラフに表示する要素 let lineChartEntry : [ChartDataEntry] = [ ChartDataEntry(x: 0, y: 0), ChartDataEntry(x: 1, y: 1), ChartDataEntry(x: 2, y: 2), ChartDataEntry(x: 3, y: 3), ChartDataEntry(x: 4, y: 4), ChartDataEntry(x: 5, y: 5), ] let chart = LineChartView() let data = LineChartData() let dataSet = LineChartDataSet(entries: lineChartEntry) // データセットを作ってチャートに反映 data.addDataSet(dataSet) chart.data = data return chart } } struct ContentView: View { var body: some View { LineChart() } } おわりに 無事SwiftUIでChartsを使うことができました! 次回は値を動的にグラフに反映する方法やグラフの見た目をカスタマイズする方法を投稿したいと思います。