- 投稿日:2020-05-26T22:40:30+09:00
【Swift】TableViewでカスタムセルを作成した場合
- 投稿日:2020-05-26T22:18:35+09:00
Composable Architectureがすごいので紹介したい
はじめに
Swiftを用いたiOS/macOS/tvOS/watchOSアプリ開発用に Composable Architecture (現在のver0.2.0)というのがあり、それがめっちゃ良いやん!と思ったのでそれを書いておきます。フレームワーク的に提供されていますが、特にSwiftUIを使ってどういう構成でアプリを作ろうかと考えている人がコードを読むだけでもとてもいい教材になると思います。
Composable Archtectureは関数型プログラミングとSwiftについての動画コンテンツ提供している Point-Free で考えられたようです。
https://www.pointfree.co/episodes/ep100-a-tour-of-the-composable-architecture-part-1必須要件
Composable Architectureの必須要件を先に書いておきます。
- iOS13以上 (macOS, tvOS, watchOSでも使用できる)
- SwiftUI, Combineが使える必要がある
- UIKitにも対応している
概要
Composable Architectureをまずざっくり説明すると、Apple製品のSwift/Combine/UIKitでのアプリ開発におけるシステムアーキテクチャを提供してくれてるわけです。というよりこの仕組を取り入れるということは開発のフレームワークと言ったほうがいいかもしれないですね。
概要を箇条書きにします。
- View以外のコンポーネントをViewStore, Store, Reducerとしている
- ViewStore
- Viewからアクションを送り結果をViewに反映できる
- Store
- StateとActionを持ち、ActionによりReducerを呼び出してStateを変更して結果をViewStoreに反映する
- Reducer
- Stateを変更するコンポーネント化された処理として定義され、小さな処理を組み合わせることもできる
- StoreをView用とするViewStoreに変換するためのWithViewStoreなどもある
- WithViewStoreがSwiftUI.Viewに準拠しておりViewのように扱える
- SubViewをWithViewStoreのbodyに記述していく
コンポーネントを図にすると次のようになるでしょう。
良い点
良い点も超ざっくり説明すると、ReduxっぽさがありながらそれをSwiftUI/Combineの仕組みを上手く使って各コンポーネント間のやりとりをしています。
- Reduxっぽい
- Redcuerを細かく分割し、画面によって必要があればそれを組み合わせる
- 巨大なグローバルReducerを作るわけではない
- 画面ごとにReducerがあり、それは別画面で組み合わせても良い
- Combineをうまく使っている点がある
- 変化をPublisherを使って反映したり
- Effectをsinkして再帰的にReducerを呼び出したりする
- SwiftUIをうまく使っている点がある
- WithViewStoreでSubView用にStoreを変換しているのが自然でいい
- 副作用をPublisherに準拠するEffect型として取り扱う
- いわゆるビジネスロジックをEffectとして取り扱う
登場するパーツやコンポーネントの説明
Effect
ロジックをCombine.Publisherとしてつなぐためのラッパーとしていい感じに作られています。
- 副作用のPublisherを定義する
- 例えば通信、ファイルアクセスなどをEffect型としている
- キャンセルがわかりやすい
- Reducerで処理のidを決めておく
- グローバルにCancellablesを配列で保持しておりそれをidでキャンセル
具体的には、副作用の結果を取得する際にそのまま直接Publisher/Futureを使うようなことはしません。もちろんEffectの実行時にFutureを選択することはあります。
Effect<Int, Never>.future { callback in DispatchQueue.main.asyncAfter(deadline: .now() + 1) { callback(.success(42)) callback(.success(1729)) // Will not be emitted by the effect } }他には既存のPublisherをEfffectに変換することも可能です。このために既存のPublisherを渡されて初期化する場合は
upstream
することで対応しています。public struct Effect<Output, Failure: Error>: Publisher { public let upstream: AnyPublisher<Output, Failure> public init<P: Publisher>(_ publisher: P) where P.Output == Output, P.Failure == Failure { self.upstream = publisher.eraseToAnyPublisher() } public func receive<S>( subscriber: S ) where S: Combine.Subscriber, Failure == S.Failure, Output == S.Input { self.upstream.receive(subscriber: subscriber) } ... }WithViewStore
- SwiftUI.View
- こいつが
@ObservedObject
を用意する具体的にはカウンタ用のSwiftUIのViewがあったとして、そいつはObservedObjectは持たない。その中でWithViewStoreを用意し、storeを渡すようにする。そしてWithViewStoreのサブビューが実際の描画用のViewになる。
struct CounterView: View { let store: Store<CounterState, CounterAction> // この外のViewではObservedObjectは持たない var body: some View { WithViewStore(self.store) { viewStore in HStack { Button("−") { viewStore.send(.decrementButtonTapped) } Text("\(viewStore.count)") .font(Font.body.monospacedDigit()) Button("+") { viewStore.send(.incrementButtonTapped) } } } } }この例だとマイナスのButtonとTextとプラスのButtonがある。WithViewStoreはSwiftUI.Viewであり、こいつがObservedObjectを保持する。
public struct WithViewStore<State, Action, Content>: View where Content: View { private let content: (ViewStore<State, Action>) -> Content private var prefix: String? @ObservedObject private var viewStore: ViewStore<State, Action> ... public init( _ store: Store<State, Action>, removeDuplicates isDuplicate: @escaping (State, State) -> Bool, @ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content ) { self.content = content self.viewStore = ViewStore(store, removeDuplicates: isDuplicate) } }Viewの外からやってきたstoreは
@ObservedObject
のViewStore型に渡される。ViewStore
- UIからのアクションをStore以降に送れる
- ObservableObjectに準拠している
@Published
によりSwiftUI.Viewを更新することができる@dynamicMemberLookup public final class ViewStore<State, Action>: ObservableObject { /// A publisher of state. public let publisher: StorePublisher<State> private var viewCancellable: AnyCancellable? /// Initializes a view store from a store. /// /// - Parameters: /// - store: A store. /// - isDuplicate: A function to determine when two `State` values are equal. When values are /// equal, repeat view computations are removed. public init( _ store: Store<State, Action>, removeDuplicates isDuplicate: @escaping (State, State) -> Bool ) { let publisher = store.$state.removeDuplicates(by: isDuplicate) self.publisher = StorePublisher(publisher) self.state = store.state self._send = store.send self.viewCancellable = publisher.sink { [weak self] in self?.state = $0 } } /// The current state. @Published public internal(set) var state: State let _send: (Action) -> Void ... }Store
- StateとActionを持つ
- ActioinによるReducerを呼び出しStateを変更
- Stateが変化するとViewStoreに反映
- 一つの画面に一つのStateの塊
- Stateの子供もしくはNestedStateとして配列を保持できる
- scopeによって複数ステートを分けて扱える
Reducer
- 画面ごとにReducerがある
- その画面で実行する処理をまとめられる
- 例
- ボタンタップしてカウントアップ
- キャンセル処理
- 通信結果の成功と失敗のハンドリング
- 別画面に使ったReducerを組み合わせたりできる
- Reducerの処理の戻り値はEffectなのでsinkできる
- Effectの結果で再帰的にReduderを呼び出せる
- Reducerに処理のハンドリングを任せる
- エラーハンドリングもReducer
- ハンドリングしたらstateを変える
- stateが変わったらStore → ViewStoreと変更が伝わりViewが変わる
CounterアプリのカウントをアップダウンするだけのシンプルなReducerであるcounterReducer
let counterReducer = Reducer<CounterState, CounterAction, CounterEnvironment> { state, action, _ in switch action { case .decrementButtonTapped: state.count -= 1 return .none case .incrementButtonTapped: state.count += 1 return .none } }やっていることはActionに応じてStateを変更している。return している
.none
はEffectのEmptyを意味していて、これをsinkしても意味がないことを示している。returnをする際にEffectを指定する例はCounterで用意した数字を基にWebAPIを呼び出すeffectsBasicsReducerがわかりやすい。
let effectsBasicsReducer = Reducer< EffectsBasicsState, EffectsBasicsAction, EffectsBasicsEnvironment > { state, action, environment in switch action { case .decrementButtonTapped: state.count -= 1 state.numberFact = nil // Return an effect that re-increments the count after 1 second. return Effect(value: EffectsBasicsAction.incrementButtonTapped) .delay(for: 1, scheduler: environment.mainQueue) .eraseToEffect() case .incrementButtonTapped: state.count += 1 state.numberFact = nil return .none case .numberFactButtonTapped: state.isNumberFactRequestInFlight = true state.numberFact = nil // Return an effect that fetches a number fact from the API and returns the // value back to the reducer's `numberFactResponse` action. return environment.numberFact(state.count) .receive(on: environment.mainQueue) .catchToEffect() .map(EffectsBasicsAction.numberFactResponse) case let .numberFactResponse(.success(response)): state.isNumberFactRequestInFlight = false state.numberFact = response return .none case .numberFactResponse(.failure): state.isNumberFactRequestInFlight = false return .none } }Actionが
.numberFactButtonTapped
の場合に戻り値をenvironment.numberFact()
としてEffectを作り、結果をmapしてEffectsBasicsAction.numberFactResponse
とすることで、sinkした結果を元にして、このeffectsBasicsReducerが再度呼び出されて action.numberFactResponse
のsuccessかfailureが呼び出される。さらに小さなcounterReducerは他のReducerでも使うことができる。次のtowCountersReducerは一つの画面にViewとして2つのカウンタがある。
let twoCountersReducer = Reducer<TwoCountersState, TwoCountersAction, TwoCountersEnvironment> .combine( counterReducer.pullback( state: \TwoCountersState.counter1, action: /TwoCountersAction.counter1, environment: { _ in CounterEnvironment() } ), counterReducer.pullback( state: \TwoCountersState.counter2, action: /TwoCountersAction.counter2, environment: { _ in CounterEnvironment() } ) )counterReducerをpullbackし、それらをcombineすることで一つのReducerにするという例となっている。
その他
複数stateを階層化できる?
- 配列としてStateを保持していくこともできる
- 並列にNestedStateを持てるし(初期画面に3つ並べる)
- 一つのStateを掘り下げていくこともできる
- サンプル
- Recursive state and actions
Bindingはどうやるの?
- View側でStoreを直接編集させない
- (set)テキスト変更時
- Actionをストアに送信することで変更を発生させる
- (get)テキスト取得時
- stateの内容を取得する
TextField( "Type here", text: viewStore.binding( get: { $0.text }, send: BindingBasicsAction.textChange ) )public func binding<LocalState>( get: @escaping (State) -> LocalState, send localStateToViewAction: @escaping (LocalState) -> Action ) -> Binding<LocalState> { Binding( get: { get(self.state) }, set: { newLocalState, transaction in withAnimation(transaction.disablesAnimations ? nil : transaction.animation) { self.send(localStateToViewAction(newLocalState)) } }) }Reducerのキャンセル
- キャンセルもReducerに処理を渡す
- そもそもReducerでEffectのCancellableは
var cancellationCancellables: [AnyHashable: [UUID: AnyCancellable]] = [:]
に保存している- Reducerでidを作成
- idは都度インスタンスを作っているがHashableなので同じ
- Hashableはメンバが同じなら同様になるんだろうがカラなのでそれでも同じ
- キャンセルのReducerではidによってキャンセルする処理を選ぶ
let effectsCancellationReducer = Reducer< EffectsCancellationState, EffectsCancellationAction, EffectsCancellationEnvironment > { state, action, environment in struct TriviaRequestId: Hashable {} switch action { case .cancelButtonTapped: state.isTriviaRequestInFlight = false return .cancel(id: TriviaRequestId()) case let .stepperChanged(value): state.count = value state.currentTrivia = nil state.isTriviaRequestInFlight = false return .cancel(id: TriviaRequestId()) case .triviaButtonTapped: state.currentTrivia = nil state.isTriviaRequestInFlight = true return environment.trivia(state.count) .receive(on: environment.mainQueue) .catchToEffect() .map(EffectsCancellationAction.triviaResponse) .cancellable(id: TriviaRequestId()) case let .triviaResponse(.success(response)): state.isTriviaRequestInFlight = false state.currentTrivia = response return .none case .triviaResponse(.failure): state.isTriviaRequestInFlight = false return .none } }興味深いのはHashableに準拠したTriviaRequestId型をインスタンス化したものをidとしているが、TriviaRequestId型はカラでメンバーがない。つまり、このカラの構造体のインスタンスを比較すると常に同じとなります。
TriviaRequestId() == TriviaRequestId()リファレンスを読むともちろんこれは正常な動作なんだけど、普段私がHashableを使うのは内部の値を気にせず型を作る時、それを比較するようにするのに使うわけです。
これの何が私に興味深いかというと、普段みなさんとしたらWeb APIで取得したIntのUserIDとかをアプリ側で型にして使うはずで、その時に比較可能にしようとHashable使うと思うんだけど、メンバをカラにしてハッシュ化する対象なけりゃ同じになるという。それってEmpty的な意味合いになって都合も合う pic.twitter.com/ub6a5PPtgc
— imajô (@yimajo) May 25, 2020内部をカラにすることでそのカラのインスタンスだけで比較することができるというのは理にかなっていて興味深いですよね。
おわりに
Composable ArchitectureはSwiftUI専用ってわけではないですが、SwiftUIを使ってアプリを開発する上でどのような構成にするのかという一つの指針になるはずです。
- 投稿日:2020-05-26T22:18:35+09:00
Swiftによるアプリ開発のためのComposable Architectureがすごいので紹介したい
はじめに
Swiftを用いたiOS/macOS/tvOS/watchOSアプリ開発用に Composable Architectureというのがあり、それがめっちゃ良いやん!と思ったのでそれを書いておきます。フレームワーク的に提供されていますが、特にSwiftUIを使ってどういう構成でアプリを作ろうかと考えている人がコードを読むだけでもとてもいい教材になると思います。
Composable Archtectureを提案しているのは、関数型プログラミングとSwiftについての動画コンテンツ提供しているPoint-Freeというサービスを提供している方たちです。
必須要件
Composable Architectureの必須要件を先に書いておきます。
- iOS13以上 (macOS, tvOS, watchOSでも使用できる)
- SwiftUI, Combineが使える必要がある
- UIKitにも対応している
この記事時点でのバージョンは0.2.0です。
概要
Composable Architectureをまずざっくり説明すると、Apple製品のSwift/Combine/UIKitでのアプリ開発におけるシステムアーキテクチャをフレームワークのかたちで提供してくれると言ったほうがいいかもしれないですね。
概要を箇条書きにします。
- View以外のコンポーネントをViewStore, Store, Reducerとしている
- ViewStore
- Viewからアクションを送り結果をViewに反映できる
- Store
- StateとActionを持ち、ActionによりReducerを呼び出してStateを変更して結果をViewStoreに反映する
- Reducer
- Stateを変更するコンポーネント化された処理として定義され、小さな処理を組み合わせることもできる
- StoreをView用とするViewStoreに変換するためのWithViewStoreなどもある
- WithViewStoreがSwiftUI.Viewに準拠しておりViewのように扱える
- SubViewをWithViewStoreのbodyに記述していく
コンポーネントを図にしてみると次のような感じです。
公式の作画ではなく私の作画なので間違いあるかもしれません。その場合はコメントなどをください。
良い点
良い点も超ざっくり説明すると、ReduxっぽさがありながらそれをSwiftUI/Combineの仕組みを上手く使って各コンポーネント間のやりとりをしています。
- ReduxっぽいがRedcuerを細かく分割していて、画面によって必要があればそれを組み合わせる
- 巨大なグローバルReducerを作るわけではない
- 画面ごとにReducerがあり、それは別画面で組み合わせても良い
- Combineをうまく使っている点がある
- 変化をPublisherを使って反映したり
- Effectをsinkして再帰的にReducerを呼び出したりする
- SwiftUIをうまく使っている点がある
- WithViewStoreでSubView用にStoreを変換しているのが自然でいい
- 副作用をPublisherに準拠するEffect型として取り扱う
- いわゆるビジネスロジックをEffectとして取り扱う
登場するパーツやコンポーネントの説明
Effect
ロジックをCombine.PublisherとしてつなぐためのラッパーとしてEffect型というのをいい感じに作られています。
- 副作用のPublisherを定義する
- 例えば通信、ファイルアクセスなどをEffect型としている
- キャンセルがわかりやすい
- Reducerで処理のidを決めておく
- グローバルにCancellablesを配列で保持しておりそれをidでキャンセル
具体的には、副作用の結果を取得する際にそのまま直接Publisher/Futureを使うようなことはしません。もちろんEffectの実行時にFutureを選択することはあります。
EffectからFutureを使ってるサンプルは次のような感じ
Effect<Int, Never>.future { callback in DispatchQueue.main.asyncAfter(deadline: .now() + 1) { callback(.success(42)) callback(.success(1729)) // Will not be emitted by the effect } }他には既存のPublisherをEffectに変換することも可能です。このために既存のPublisherを渡されて初期化する場合は、Publisherを
upstream
とし、それをSubscriberにreceiveすることで対応しています。public struct Effect<Output, Failure: Error>: Publisher { public let upstream: AnyPublisher<Output, Failure> public init<P: Publisher>(_ publisher: P) where P.Output == Output, P.Failure == Failure { self.upstream = publisher.eraseToAnyPublisher() } public func receive<S>( subscriber: S ) where S: Combine.Subscriber, Failure == S.Failure, Output == S.Input { self.upstream.receive(subscriber: subscriber) } ... }WithViewStore
- SwiftUI.View
- こいつが
@ObservedObject
を用意する例えばカウンタ用のSwiftUIのViewがあったとします
その画面を示す大外のViewはObservedObjectは持たず、その中のカウンター部分でWithViewStoreを用意し、storeを渡すようにする。そしてWithViewStoreのサブビューが実際の描画用のViewになります。
struct CounterView: View { let store: Store<CounterState, CounterAction> // この大外のViewではObservedObjectは持たない var body: some View { // ↓このWithViewStoreがObservedObjectを持ちます WithViewStore(self.store) { viewStore in HStack { // サブビューたち Button("−") { viewStore.send(.decrementButtonTapped) } Text("\(viewStore.count)") .font(Font.body.monospacedDigit()) Button("+") { viewStore.send(.incrementButtonTapped) } } } } }WithViewStoreはSwiftUI.Viewであり、こいつがObservedObjectを保持し、
サブビューとしてマイナスのButtonとTextとプラスのButtonがあります。WithViewStoreはフレームワークとして提供されます。下記に省略しつつ紹介します。
public struct WithViewStore<State, Action, Content>: View where Content: View { private let content: (ViewStore<State, Action>) -> Content private var prefix: String? @ObservedObject private var viewStore: ViewStore<State, Action> ... public init( _ store: Store<State, Action>, removeDuplicates isDuplicate: @escaping (State, State) -> Bool, @ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content ) { self.content = content self.viewStore = ViewStore(store, removeDuplicates: isDuplicate) } }WithViewStoreによってViewの外からやってきたstoreは
@ObservedObject
のViewStore型に渡されます。ViewStore
- UIからのアクションをStore以降に送れる
- ObservableObjectに準拠している
@Published
によりSwiftUI.Viewを更新することができる@dynamicMemberLookup public final class ViewStore<State, Action>: ObservableObject { /// A publisher of state. public let publisher: StorePublisher<State> private var viewCancellable: AnyCancellable? /// Initializes a view store from a store. /// /// - Parameters: /// - store: A store. /// - isDuplicate: A function to determine when two `State` values are equal. When values are /// equal, repeat view computations are removed. public init( _ store: Store<State, Action>, removeDuplicates isDuplicate: @escaping (State, State) -> Bool ) { let publisher = store.$state.removeDuplicates(by: isDuplicate) self.publisher = StorePublisher(publisher) self.state = store.state self._send = store.send self.viewCancellable = publisher.sink { [weak self] in self?.state = $0 } } /// The current state. @Published public internal(set) var state: State let _send: (Action) -> Void ... }Store
- StateとActionを持つ
- ActioinによるReducerを呼び出しStateを変更
- Stateが変化するとViewStoreに反映
- 一つの画面に一つのStateの塊
- Stateの子供もしくはNestedStateとして配列を保持できる
- scopeによって複数ステートを分けて扱える
Reducer
- 画面ごとにReducerがある
- その画面で実行する処理をまとめられる
- 例
- ボタンタップしてカウントアップ
- キャンセル処理
- 通信結果の成功と失敗のハンドリング
- 別画面に使ったReducerを組み合わせたりできる
- Reducerの処理の戻り値はEffectなのでsinkできる
- Effectの結果で再帰的にReduderを呼び出せる
- Reducerに処理のハンドリングを任せる
- エラーハンドリングもReducer
- ハンドリングしたらstateを変える
- stateが変わったらStore → ViewStoreと変更が伝わりViewが変わる
CounterアプリのカウントをアップダウンするだけのシンプルなReducerであるcounterReducer
let counterReducer = Reducer<CounterState, CounterAction, CounterEnvironment> { state, action, _ in switch action { case .decrementButtonTapped: state.count -= 1 return .none case .incrementButtonTapped: state.count += 1 return .none } }やっていることはActionに応じてStateを変更している。return している
.none
はEffectのEmptyを意味していて、これをsinkしても意味がないことを示している。returnをする際にEffectを指定する例はCounterで用意した数字を基にWebAPIを呼び出すeffectsBasicsReducerがわかりやすい。
let effectsBasicsReducer = Reducer< EffectsBasicsState, EffectsBasicsAction, EffectsBasicsEnvironment > { state, action, environment in switch action { case .decrementButtonTapped: state.count -= 1 state.numberFact = nil // Return an effect that re-increments the count after 1 second. return Effect(value: EffectsBasicsAction.incrementButtonTapped) .delay(for: 1, scheduler: environment.mainQueue) .eraseToEffect() case .incrementButtonTapped: state.count += 1 state.numberFact = nil return .none case .numberFactButtonTapped: state.isNumberFactRequestInFlight = true state.numberFact = nil // Return an effect that fetches a number fact from the API and returns the // value back to the reducer's `numberFactResponse` action. return environment.numberFact(state.count) .receive(on: environment.mainQueue) .catchToEffect() .map(EffectsBasicsAction.numberFactResponse) case let .numberFactResponse(.success(response)): state.isNumberFactRequestInFlight = false state.numberFact = response return .none case .numberFactResponse(.failure): state.isNumberFactRequestInFlight = false return .none } }Actionが
.numberFactButtonTapped
の場合に戻り値をenvironment.numberFact()
としてEffectを作り、結果をmapしてEffectsBasicsAction.numberFactResponse
とすることで、sinkした結果を元にして、このeffectsBasicsReducerが再度呼び出されて action.numberFactResponse
のsuccessかfailureが呼び出される。さらに小さなcounterReducerは他のReducerでも使うことができる。次のtowCountersReducerは一つの画面にViewとして2つのカウンタがある。
let twoCountersReducer = Reducer<TwoCountersState, TwoCountersAction, TwoCountersEnvironment> .combine( counterReducer.pullback( state: \TwoCountersState.counter1, action: /TwoCountersAction.counter1, environment: { _ in CounterEnvironment() } ), counterReducer.pullback( state: \TwoCountersState.counter2, action: /TwoCountersAction.counter2, environment: { _ in CounterEnvironment() } ) )counterReducerをpullbackし、それらをcombineすることで一つのReducerにするという例となっている。
その他
複数stateを階層化できる?
- 配列としてStateを保持していくこともできる
- 並列にNestedStateを持てるし(初期画面に3つ並べる)
- 一つのStateを掘り下げていくこともできる
- サンプル
- Recursive state and actions
Bindingはどうやるの?
- View側でStoreを直接編集させない
- (set)テキスト変更時
- Actionをストアに送信することで変更を発生させる
- (get)テキスト取得時
- stateの内容を取得する
TextField( "Type here", text: viewStore.binding( get: { $0.text }, send: BindingBasicsAction.textChange ) )public func binding<LocalState>( get: @escaping (State) -> LocalState, send localStateToViewAction: @escaping (LocalState) -> Action ) -> Binding<LocalState> { Binding( get: { get(self.state) }, set: { newLocalState, transaction in withAnimation(transaction.disablesAnimations ? nil : transaction.animation) { self.send(localStateToViewAction(newLocalState)) } }) }Reducerのキャンセル
- キャンセルもReducerに処理を渡す
- そもそもReducerでEffectのCancellableは
var cancellationCancellables: [AnyHashable: [UUID: AnyCancellable]] = [:]
に保存している- Reducerでidを作成
- idは都度インスタンスを作っているがHashableなので同じ
- Hashableはメンバが同じなら同様になるんだろうがカラなのでそれでも同じ
- キャンセルのReducerではidによってキャンセルする処理を選ぶ
let effectsCancellationReducer = Reducer< EffectsCancellationState, EffectsCancellationAction, EffectsCancellationEnvironment > { state, action, environment in struct TriviaRequestId: Hashable {} switch action { case .cancelButtonTapped: state.isTriviaRequestInFlight = false return .cancel(id: TriviaRequestId()) case let .stepperChanged(value): state.count = value state.currentTrivia = nil state.isTriviaRequestInFlight = false return .cancel(id: TriviaRequestId()) case .triviaButtonTapped: state.currentTrivia = nil state.isTriviaRequestInFlight = true return environment.trivia(state.count) .receive(on: environment.mainQueue) .catchToEffect() .map(EffectsCancellationAction.triviaResponse) .cancellable(id: TriviaRequestId()) case let .triviaResponse(.success(response)): state.isTriviaRequestInFlight = false state.currentTrivia = response return .none case .triviaResponse(.failure): state.isTriviaRequestInFlight = false return .none } }興味深いのはHashableに準拠したTriviaRequestId型をインスタンス化したものをidとしているが、TriviaRequestId型はカラでメンバーがない。つまり、このカラの構造体のインスタンスを比較すると常に同じとなります。
TriviaRequestId() == TriviaRequestId()リファレンスを読むともちろんこれは正常な動作なんだけど、普段私がHashableを使うのは内部の値を気にせず型を作る時、それを比較するようにするのに使うわけです。
これの何が私に興味深いかというと、普段みなさんとしたらWeb APIで取得したIntのUserIDとかをアプリ側で型にして使うはずで、その時に比較可能にしようとHashable使うと思うんだけど、メンバをカラにしてハッシュ化する対象なけりゃ同じになるという。それってEmpty的な意味合いになって都合も合う pic.twitter.com/ub6a5PPtgc
— imajô (@yimajo) May 25, 2020内部をカラにすることでそのカラのインスタンスだけで比較することができるというのは理にかなっていて興味深いですよね。
おわりに
Composable ArchitectureはSwiftUI専用ってわけではないですが、SwiftUIを使ってアプリを開発する上でどのような構成にするのかという一つの指針になるはずです。
- 投稿日:2020-05-26T21:59:44+09:00
【プログラミング初心者】Swift基礎~構造体・クラス~
型を定義する
以前Swiftにおける基本となる型について説明しました。
この基本型とif文とfor文さえあればプログラミングはできるのですが、これだけだとプログラムは煩雑になり管理しづらくなります。
そこである1つのデータや処理のまとまりを新しい型として定義することで全体の見通しをよくすることができます。
その型定義の方法に構造体やクラスという手法があります。構造体
構造体の定義
構造体を定義するための構文は以下となります。
struct 型名 { ... }もう少し具体的な実装をします。会社の構造を例にしてみましょう。
会社を表す型としてCompany
という構造体を作成します。
Company.swift
というファイルを作成し、構造体を定義します。Company.swiftstruct Company { }これで
Company
という構造体が定義されました。
実際にViewController.swift
などから使ってあげましょう。
Int
の実態は数値なので変数には数値を代入すればよかったのですが、構造体の実態はオブジェクトと呼ばれるデータのまとまりになります。
そのためInt
とは少し違い作成する構文は構造体名()
でオブジェクトを作成します。ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() var company: Company = Company() }メンバ変数
さらにここから
Company
を表す属性を定義していきます。
会社を表すためにはどんな項目が必要でしょうか?
まず会社名、社長、あとは売り上げあたりを定義してみましょう。Company.swiftstruct Company { // 会社名 var name: String // 社長 var president: String // 売り上げ var earnings: Int }これで
Company
型はname, president, earningsという会社を表す属性を持つことになります。
これらの構造体を表すために定義した変数のことをCompanyのメンバ変数、もしくは単にメンバと呼びます。次に
ViewController.swift
をもう一度見てみましょう。
初期化の行でMissing arguments for parameters 'name', 'president', 'earnings' in call
というエラーが発生しています。
「Companyに必要なメンバの実態がないので教えてください」というようなエラーです。
メンバを初期化時に入れてあげましょう。
エラーの左にある赤い[◎]をクリックし[Fix]を選択してあげると初期化のテンプレートをXcodeが作成してくれます。
メンバがある場合の初期化の方法は構造体(メンバ1: 値, メンバ2: 値, ...)
です。ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() var company: Company = Company(name: "株式会社山田商事", president: "山田太郎", earnings: 1000000) }さてこれで「株式会社山田商事」という会社が作られました。
メンバ変数を参照・更新してみましょう。
メンバへアクセスする構文は変数.メンバ
です。ViewController.swiftvar company: Company = Company(name: "株式会社山田商事", president: "山田太郎", earnings: 1) print(company.name) // "株式会社山田商事" print(company.president) // "山田太郎" print(company.corporateNumber) // 1000000 company.name = "山田次郎" print(company.president) // "山田次郎"またメンバ変数はさらに別の構造体でも問題ありません。
会社のすぐ下には部署がぶら下がっていると仮定し、
Department
という構造体をDepartment.swift
に定義します。
部署はとりあえず部署名と部長がいるだけにしましょうか。会社の部署は複数の可能性があるので
Department
の配列で定義します。Department.swiftstruct Department { // 部署名 var name: String // 部長 var manager: String }Company.swiftstruct Company { // 会社名 var name: String // 社長 var president: String // 法人番号 var corporateNumber: Int // 部署 var departments: [Department] }ViewController.swift// 部署の作成 let developDepartment: Department = Department(name: "開発部", manager: "開発太郎") let salesDepartment: Department = Department(name: "営業部", manager: "営業太郎") // 会社の作成 var company: Company = Company(name: "株式会社山田商事", president: "山田太郎", earnings: 1000000, departments: [developDepartment, salesDepartment])部署へのアクセスはメンバのメンバにアクセスするように
.
を繋げていきます。ViewController.swift// 登録した部署にアクセス print(company.departments[0].name) // "開発部"メソッド
構造体には振る舞い、つまり構造体が行う処理を定義することができます。
この振る舞いをメソッドと呼びます。メソッド定義の構文は以下となります。
(正確ではありますが関数と呼ぶこともあります)func メソッド名(引数) { ... }メソッドには引数と呼ばれるパラメータを与えることができます。
引数は引数名: 型
というように(arg1: String, arg2: Int, ...)
と定義します。会社の振る舞いを考えてみましょう。
まず会社は売り上げをあげていくものなので、月の売り上げを加算するメソッドを定義します。Company.swiftstruct Company { // 会社名 var name: String // 社長 var president: String // 売り上げ var earnings: Int // 部署 var departments: [Department] /// 売り上げを計上する /// - Parameter earnings: 月売り上げ mutating func sumUp(monthlyEarnings: Int) { self.earnings = self.earnings + monthlyEarnings } }少し注意が必要なのがfuncの前に
mutating
というものがあります。
これは構造体特有のものですが、今回sumUp
の中でearnings
というメンバ変数を更新しています。
このようなメンバ変数を更新するメソッドを定義する場合は頭にmutating
をつけます。
mutating
がついたメソッドは構造体が変数、つまりvar
を使って宣言されていなければならないというルールがあります。
配列のappend
やremove
と同じです。
少し脱線しますが実は配列も構造体として定義されています。
append
などの定義を見るとmutating
がついていることを確認できます。
配列構造体は配列自体はメンバ変数として保持しています。そのためappend
を呼び出すとメンバ変数が更新されるためmutating
として定義されているというわけです。さてそれでは定義したメソッドを呼び出してみましょう。
ViewController.swiftvar company: Company = Company(name: "株式会社山田商事", president: "山田太郎", earnings: 1000000, departments: []) print(company.earnings) // 1000000 company.sumUp(monthlyEarnings: 1000000) print(company.earnings) // 2000000 company.sumUp(monthlyEarnings: 200000) print(company.earnings) // 2200000これでメソッドを呼び出し値を更新できました。
メソッド化する意味
メソッド呼び出したことで
company
の状態を更新できましたが、そんなことをしなくても更新できるのでは?と思う人もいるかもしれません。
そうです。先程紹介したメンバ変数にアクセスして更新する方法でも当然更新することができます。ViewController.swiftprint(company.earnings) // 1000000 company.earnings = company.earnings + 1000000 print(company.earnings) // 2000000 company.earnings = company.earnings + 200000 print(company.earnings) // 2200000ではメソッド化することでどんなメリットがあるのでしょうか?
まず1つ目は処理に名前を付けられるという点があります。
メソッドとして定義することでsumUp(monthlyEarnings: Int)
という処理ができ、メソッド名自体がどんな処理をするのか想像することができます。
これは変数に関しても同様のことが言えます。2つ目は処理をまとめられるという点です。
先程のコードを見てください。簡単な処理ではありますがprint(company.earnings)
が何度も出てきます。
このような共通した処理が複数行に渡るとコード自体がかなり冗長になります。
メソッド化しておくと一度書いておくだけで何度も呼び出せます。Company.swift/// 売り上げを計上する /// - Parameter earnings: 月売り上げ mutating func sumUp(monthlyEarnings: Int) { print("before: \(self.earnings)") self.earnings = self.earnings + monthlyEarnings print("after: \(self.earnings)") print("----------") }ViewController.swiftvar company: Company = Company(name: "株式会社山田商事", president: "山田太郎", earnings: 1000000, departments: []) company.sumUp(monthlyEarnings: 1000000) company.sumUp(monthlyEarnings: 200000)実行結果before: 1000000 after: 2000000 ---------- before: 2000000 after: 2200000 ----------3つ目はコードの変更に強くなるということです。
結局は2つ目のメリットと同じようなものですが、例えば今は単純に売り上げを足しているところを消費税も合計に入れたいという要件が出たとします。
複数箇所で計算していた場合全ての処理を修正しなければいけません。
これをメソッドを呼び出して処理をしておくとsumUp
の中身だけを修正すればよく、呼び出し元のViewController.swift
には全く触れる必要がありません。Company.swiftmutating func sumUp(monthlyEarnings: Int) { print("before: \(self.earnings)") // ここだけを修正する let tax = Int(Double(monthlyEarnings) * 1.1) self.earnings = self.earnings + monthlyEarnings + tax print("after: \(self.earnings)") print("----------") }ViewController.swiftvar company: Company = Company(name: "株式会社山田商事", president: "山田太郎", earnings: 1000000, departments: []) company.sumUp(monthlyEarnings: 1000000) company.sumUp(monthlyEarnings: 200000)実行結果before: 1000000 after: 3100000 ---------- before: 3100000 after: 3520000 ----------このように構造体に処理を持たせておくことで、コードの可読性が上がりさらに変更に強いコードにすることができるのです。
クラス
クラスも構造体と似たようなものでメンバ変数とメソッドを持つデータ構造を表します。
違いは後で次項で説明するので本項では定義の仕方について説明します。クラスの定義
クラスを定義するための構文は以下となります。基本的には構造体と同じです。
class クラス名 { ... }今度は人間クラスというものを定義し、オブジェクトを作ってみましょう。
作り方も構造体と同じです。Human.swiftclass Human { }ViewController.swiftvar human = Human()これで人間のオブジェクトを作成することができました。
クラスから作成されたオブジェクトのことは特にインスタンスと呼びます。
ですが経験上あまり明確に区別して話す機会はないように思うので基本的にはオブジェクトと言っておけば問題ないでしょう。一応オブジェクト、インスタンスそれぞれの意味は
- オブジェクト
- プログラム上で扱うデータ全般。その実体化したデータを1つのモノとして見る考え方。
- インスタンス
- 何らかのクラスに基づいて実体化されたオブジェクト
となりますのでインスタンスもつまりはオブジェクトの一種です。
なのでインスタンスをオブジェクトと言っても誤りではないというわけです。プロパティ/メソッド
クラスも構造体と同じようにメンバ変数とメソッドを持たせることができます。
厳密には違いますがクラスで定義したメンバ変数をプロパティと呼ぶととりあえずは覚えてください。定義の仕方も同様です。
まずはプロパティを定義します。Human.swiftclass Human { // 名前 var name: String // 年齢 var age: Int }すると以下のようなエラーが発生します。
Class 'Human' has no initializers「イニシャライザがありません」というようなエラーです。
イニシャライザとは初期化の際に使用する、少し特殊なメソッドです
別の言語ではコンストラクタといい、つまりクラスを構成する人というイメージです。構造体のときはこのエラーは発生しませんでしたが実は構造体は暗黙的にイニシャライザを自動で持ってくれます。
ですがクラスの場合は明示的に作ってあげる必要があります。イニシャライザの定義の仕方は色々ありますが今回は最も単純なイニシャライザを定義します。
基本構文はinit(引数)
です。Human.swiftclass Human { // 名前 var name: String // 年齢 var age: Int /// 初期化 /// - Parameters: /// - name: 初期値となる名前 /// - age: 初期値となる年齢 init(name: String, age: Int) { self.name = name self.age = age } }呼び出し方は
クラス名(引数)
です。
あまりやりませんがクラス名.init(引数)
でも呼び出せます。ViewController.swiftvar human = Human(name: "山田太郎", age: 25) var humanInit = Human.init(name: "山田太郎", age: 25)構造体のときと同じ生成の仕方ができました。
クラスも構造体もイニシャライザのみクラス名(引数)
という呼び出し方ができます。メソッドも構造体同様に定義でき、呼び出し方も同様です。
Human.swiftclass Human { // 名前 var name: String // 年齢 var age: Int /// 初期化 /// - Parameters: /// - name: 初期値となる名前 /// - age: 初期値となる年齢 init(name: String, age: Int) { self.name = name self.age = age } /// 話す func talk() { print("私の名前は\(self.name)です。") } }ViewController.swiftvar human = Human(name: "山田太郎", age: 25) human.talk()私の名前は山田太郎です。一点異なることはメソッド内でメンバ変数が更新される場合でも
mutating
は不要となります。
ここは構造体と異なる点です。Human.swift/// 年齢を更新する /// - Parameter age: 年齢 func update(age: Int) { self.age = age }このようにしてアプリで扱うデータをオブジェクトという人間がわかりやすい単位で管理していきます。
最後に
今回は新しく自分で型を定義するために構造体とクラスについて説明しました。
このようにオブジェクトベースで考えていく方法をオブジェクト指向プログラミングといいます。
どんなオブジェクトを作れば実装しやすいか、管理しやすいかを考えながら実装してみてください。また構造体、クラスどちらも似たようなものですがそれぞれ値型と参照型と言われメモリの確保のされ方が異なります。
そのあたりはまた別途記事にしていきます。本記事とは別でプログラミング未経験からiOSアプリ開発が行えるようになることを目的とした記事を連載しています。
連載は以下にまとめていますのでそちらも是非もご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684
- 投稿日:2020-05-26T21:59:33+09:00
2変数、4分岐の if 文について
変数 min_value, max_value が存在するとき、それぞれの条件で処理を変えたい
例えば…
min_value max_value 処理 null null なにもしない null not null 上限より小さいことを検証 not null null 下限より大きいことを検証 not null not null 範囲内にあることを検証 普通はこんな感じ
Main.javaclass Main { public static void main(String[] args) { } private static String hoge(int value, Integer min_value, Integer max_value) { if (min_value == null && max_value == null) { // なにもしない return ""; } if (min_value == null) { // 上限チェック return ""; } if (max_value == null) { // 下限チェック return ""; } // 範囲チェック return ""; } }
- パッと見、分かりづらい
- 比較が一つの場合と、四つの場合が存在し、データによっては速度に違いが出る
- 見た目が悪い
これをなんとか解消したい…
main.swiftfunc hoge(_ value: Int, _ min_value: Int?, _ max_value: Int?) -> String { // Optional が外れない switch(min_value != nil, max_value != nil) { case (true, true): return "" // 範囲チェック case (true, false): return "" // 上限チェック case (false, true): return "" // 下限チェック case (false, false): return "" // 何もしない } }Pytyon3 の場合
main.pydef hoge(value, min_value, max_value): command = { (True, True): lambda: "", # 範囲チェック (True, False): lambda: "", # 下限チェック (False, True): lambda: "", # 上限チェック (False, False): lambda: "", # なにもしない } return command[(min_value is not None, max_value is not None)]()
もっと良い方法がありましたら、教えていただきたいです。
- 投稿日:2020-05-26T21:05:23+09:00
UIButtonに隠しActionをつける方法
はじめに
ただどう動くんだろう?って言うだけの話で実用性は皆無です
なんか UIButton に謎のアクションを付けれたというお話です。環境
- Xcode 11.4
- iPhone 11 Pro Max (13.4)
実装
- StoryboardでFirstViewControllerとSecondViewControllerを用意する
- FirstVCにボタンをのせる
- IBActionでボタンのアクションをFirstVCに紐付ける(
tappedFirst(_ sender: Any)
)- SecondVCにFirstVCのボタンをコピペする
- IBActionでボタンのアクションをSecondVCに紐付ける(
tappedSecond(_ sender: Any)
)こんな感じ
final class FirstViewController: UIViewController { @IBAction private func tappedFirst(_ sender: Any) { print("First") let vc = storyboard?.instantiateViewController(identifier: "Second") navigationController?.pushViewController(vc!, animated: true) } } final class SecondViewController: UIViewController { @IBAction private func tappedSecond(_ sender: Any) { print("Second") } }現象
Storyboard で SecondVC のボタンを確認すると...
こうなる!
tappedFirst
がついてる。シュミレータで SecondVC のボタンを押下してみるとSecond
と print 表示される。(FirstVCのtappedFirst(_)
が呼ばれたりはしない)unrecognized selector でクラッシュもしない調査
下記のようにボタンのカスタムクラスを作り FirstVC と SecondVC のボタンに継承させる。
class CustomButton: UIButton { override func sendActions(for controlEvents: UIControl.Event) { print("sendActions") super.sendActions(for: controlEvents) } override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { print("sendAction") super.sendAction(action, to: target, for: event) } override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { print("beginTracking") return super.beginTracking(touch, with: event) } override func endTracking(_ touch: UITouch?, with event: UIEvent?) { print("endTracking") return super.endTracking(touch, with: event) } override func cancelTracking(with event: UIEvent?) { print("cancelTracking") super.cancelTracking(with: event) } override func addTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event) { print("addTarget") super.addTarget(target, action: action, for: controlEvents) } }FirstVC表示時のログ
addTargetFirstVCのボタン押下時のログ
beginTracking endTracking sendAction First addTarget最後の addTarget は SecondVC 表示時のやつ
SecondVC表示時のログ
addTargetアクションは2つあるけど1回しか呼ばれない
SecondVCのボタン押下時のログ
beginTracking endTracking sendAction Second調査結果
SecondVC のボタンの
addTarget
が一回しか呼ばれていないことからそもそもtappedFirst
のアクションは追加されていないみたいStoryboardでアクションを付けたボタンを他画面からコピペすれば動作しない謎のアクションを付けることができる!これをすれば他の開発者を惑わすこともたやすいだろう
さいごに
実装中に謎のアクションをみつけたので調べてみた結果です。活かせる場はあるだろうか
- 投稿日:2020-05-26T19:31:52+09:00
[Realmエラー] Value of type 'Realm' has no member 'objectForPrimaryKey' 解決方法
- 投稿日:2020-05-26T16:17:35+09:00
[Swift5][ FAB]フローティングアクションボタンの作り方
フローティングアクションボタンの作り方。
実装イメージ
TableViewCellの上に浮いてるデザインで、スライドしても固定されるように実装します。
Storyboard
まずbuttonのみを置いてAutoLayoutをつけます。
その上にTableView,TableViewCellを画面一杯に配置します。TableViewCellのIdentifierはCellに設定しましょう。
ソースコード
ViewController.Swiftimport UIKit class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var button: UIButton! // TableView func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 30 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) return cell } // FAB var startingFrame : CGRect! var endingFrame : CGRect! func scrollViewDidScroll(_ scrollView: UIScrollView) { if (scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height)) && self.button.isHidden { self.button.isHidden = false self.button.frame = startingFrame UIView.animate(withDuration: 1.0) { self.button.frame = self.endingFrame } } } func configureSizes() { let screenSize = UIScreen.main.bounds let screenWidth = screenSize.width let screenHeight = screenSize.height startingFrame = CGRect(x: 0, y: screenHeight+100, width: screenWidth, height: 100) endingFrame = CGRect(x: 0, y: screenHeight-100, width: screenWidth, height: 100) } override func viewDidLoad() { super.viewDidLoad() // buttonを角丸にする button.layer.cornerRadius = 32 } }
- 投稿日:2020-05-26T16:17:35+09:00
[Swift5][FAB]フローティングアクションボタンの作り方
フローティングアクションボタンの作り方。
実装イメージ
TableViewCellの上に浮いてるデザインで、スライドしても固定されるように実装します。
Storyboard
まずbuttonのみを置いてAutoLayoutをつけます。
その上にTableView,TableViewCellを画面一杯に配置します。TableViewCellのIdentifierはCellに設定しましょう。
ソースコード
ViewController.Swiftimport UIKit class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var button: UIButton! // TableView func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 30 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) return cell } // FAB var startingFrame : CGRect! var endingFrame : CGRect! func scrollViewDidScroll(_ scrollView: UIScrollView) { if (scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height)) && self.button.isHidden { self.button.isHidden = false self.button.frame = startingFrame UIView.animate(withDuration: 1.0) { self.button.frame = self.endingFrame } } } func configureSizes() { let screenSize = UIScreen.main.bounds let screenWidth = screenSize.width let screenHeight = screenSize.height startingFrame = CGRect(x: 0, y: screenHeight+100, width: screenWidth, height: 100) endingFrame = CGRect(x: 0, y: screenHeight-100, width: screenWidth, height: 100) } override func viewDidLoad() { super.viewDidLoad() // buttonを角丸にする button.layer.cornerRadius = 32 } }
- 投稿日:2020-05-26T14:54:56+09:00
[Swift5] TextFieldのあるAlertを表示させる方法
SwiftでTextFieldのあるAlertを表示させる方法。
実装イメージ
このようなタイトル、サブタイトル、入力欄、キャンセル、追加で構成されたAlertを作成して行きます。
Storyboard
buttonを押すとAlertが表示されるようにするため、好きな場所にbuttonを1つ追加しましょう。
ソースコード
ViewController.Swiftimport UIKit //UITextFieldDelegateを追加する class ViewController: UIViewController,UITextFieldDelegate { //StoryboardのbuttonをAction接続する @IBAction func aleat(_ sender: Any) { let alert = UIAlertController(title: nil, message: nil, preferredStyle: .alert) alert.title = "ここにタイトル" alert.message = "メッセージ" alert.addTextField(configurationHandler: {(textField) -> Void in textField.delegate = self }) //追加ボタン alert.addAction( UIAlertAction( title: "追加", style: .default, handler: {(action) -> Void in self.hello(action.title!) }) ) //キャンセルボタン alert.addAction( UIAlertAction( title: "キャンセル", style: .cancel, handler: {(action) -> Void in self.hello(action.title!) }) ) //アラートが表示されるごとにprint self.present( alert, animated: true, completion: { print("アラートが表示された") }) } func hello(_ msg:String){ print(msg) } override func viewDidLoad() { super.viewDidLoad() } }ビルドして確認してみましょう
実装イメージのようなAlertを表示させることができたでしょうか?
title部分は好きな文に変えることでお好みのAlertを表示させることができます。できたー!という方はLGTM押してくださると僕がめっちゃ喜びます!
- 投稿日:2020-05-26T13:46:30+09:00
【SwiftUI】 CornerRadiusを一部にのみ適用する
使い方
background
の中で呼ぶことで任意の位置で、任意のradiusを指定できます。Text("あいうえお") .padding(11) .font(.custom("HiraginoSans-W3", size: 15)) .background(RoundedCorners(color: Color.tint, tl: 0, tr: 30, bl: 30, br: 0)) .foregroundColor(.white)一部角丸ができるカスタムViewを作る
struct RoundedCorners: View { var color: Color = .blue var tl: CGFloat = 0.0 var tr: CGFloat = 0.0 var bl: CGFloat = 0.0 var br: CGFloat = 0.0 var body: some View { GeometryReader { geometry in Path { path in let w = geometry.size.width let h = geometry.size.height // Make sure we do not exceed the size of the rectangle let tr = min(min(self.tr, h/2), w/2) let tl = min(min(self.tl, h/2), w/2) let bl = min(min(self.bl, h/2), w/2) let br = min(min(self.br, h/2), w/2) path.move(to: CGPoint(x: w / 2.0, y: 0)) path.addLine(to: CGPoint(x: w - tr, y: 0)) path.addArc(center: CGPoint(x: w - tr, y: tr), radius: tr, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false) path.addLine(to: CGPoint(x: w, y: h - br)) path.addArc(center: CGPoint(x: w - br, y: h - br), radius: br, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false) path.addLine(to: CGPoint(x: bl, y: h)) path.addArc(center: CGPoint(x: bl, y: h - bl), radius: bl, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false) path.addLine(to: CGPoint(x: 0, y: tl)) path.addArc(center: CGPoint(x: tl, y: tl), radius: tl, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false) } .fill(self.color) } } }参考
https://stackoverflow.com/questions/56760335/round-specific-corners-swiftui
- 投稿日:2020-05-26T13:46:30+09:00
[SwiftUI] CornerRadiusを一部にのみ適用する
使い方
background
の中で呼ぶことで任意の位置で、任意のradiusを指定できます。Text("あいうえお") .padding(11) .font(.custom("HiraginoSans-W3", size: 15)) .background(RoundedCorners(color: Color.tint, tl: 0, tr: 30, bl: 30, br: 0)) .foregroundColor(.white)一部角丸ができるカスタムViewを作る
struct RoundedCorners: View { var color: Color = .blue var tl: CGFloat = 0.0 var tr: CGFloat = 0.0 var bl: CGFloat = 0.0 var br: CGFloat = 0.0 var body: some View { GeometryReader { geometry in Path { path in let w = geometry.size.width let h = geometry.size.height // Make sure we do not exceed the size of the rectangle let tr = min(min(self.tr, h/2), w/2) let tl = min(min(self.tl, h/2), w/2) let bl = min(min(self.bl, h/2), w/2) let br = min(min(self.br, h/2), w/2) path.move(to: CGPoint(x: w / 2.0, y: 0)) path.addLine(to: CGPoint(x: w - tr, y: 0)) path.addArc(center: CGPoint(x: w - tr, y: tr), radius: tr, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false) path.addLine(to: CGPoint(x: w, y: h - br)) path.addArc(center: CGPoint(x: w - br, y: h - br), radius: br, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false) path.addLine(to: CGPoint(x: bl, y: h)) path.addArc(center: CGPoint(x: bl, y: h - bl), radius: bl, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false) path.addLine(to: CGPoint(x: 0, y: tl)) path.addArc(center: CGPoint(x: tl, y: tl), radius: tl, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false) } .fill(self.color) } } }参考
https://stackoverflow.com/questions/56760335/round-specific-corners-swiftui
- 投稿日:2020-05-26T13:45:49+09:00
SwiftUIでNaivigationViewが重なったら
起きたこと
原因と対処法
NavigationViewが複数存在していることが原因です。
はじめのViewにのみNavigationView{}で囲むようにしましょう。ソースコード
import SwiftUI struct test12: View { var body: some View { NavigationView{ //これだけでいい NavigationLink(destination: test13()){ Text("Hello, World!") } } } } struct test13: View { var body: some View { NavigationView { //これはいらない NavigationLink(destination: test14()) { Text(/"Hello, World!") } } } } struct test14: View { var body: some View { Text("Hello, World!") } } struct test12_Previews: PreviewProvider { static var previews: some View { test12() } }
- 投稿日:2020-05-26T09:38:32+09:00
こんなソースコードはイヤだ-ifの条件が逆じゃない?
- 投稿日:2020-05-26T02:19:05+09:00
Swift Method Swizzling (initializer, class method, instance method, getter, setter)
Swift 5のSwizzleについて1つのサンプルにまとめました。
自分もデバッグや一時的な動作変更に使う程度なので、間違いや補足などありましたらご指摘願います。
UIView+Swizzle.swift
import UIKit extension UIView { private struct SwizzleStatic { static var once = true } class func swizzle() { guard SwizzleStatic.once else { return } SwizzleStatic.once = false let swizzleMethod = { (original: Selector, swizzled: Selector) in guard let originalMethod = class_getInstanceMethod(self, original), let swizzledMethod = class_getInstanceMethod(self, swizzled) else { return } method_exchangeImplementations(originalMethod, swizzledMethod) } let swizzleClassMethod = { (original: Selector, swizzled: Selector) in guard let originalMethod = class_getClassMethod(self, original), let swizzledMethod = class_getClassMethod(self, swizzled) else { assertionFailure("The methods are not found!") return } method_exchangeImplementations(originalMethod, swizzledMethod) } // イニシャライザ swizzleMethod(#selector(UIView.init(frame:)), #selector(UIView.swizzled_init(frame:))) // クラスメソッド swizzleClassMethod(#selector(UIView.animate(withDuration:delay:options:animations:completion:)), #selector(UIView.swizzled_animate(withDuration:delay:options:animations:completion:))) // インスタンスメソッド swizzleMethod(#selector(UIView.addSubview(_:)), #selector(UIView.swizzled_addSubview(_:))) // プロパティ getter swizzleMethod(#selector(getter: UIView.frame), #selector(UIView.swizzled_get_frame)) // プロパティ setter swizzleMethod(#selector(setter: UIView.frame), #selector(UIView.swizzled_set_frame(_:))) } // イニシャライザを上書き @objc func swizzled_init(frame: CGRect) -> UIView { let instance = swizzled_init(frame: frame) print("\(type(of: self)):\(#function):\(#line), \( instance )") return instance } // クラスメソッドを上書き @objc class func swizzled_animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions = [], animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) { print("\(type(of: self)):\(#function):\(#line), \( duration )") swizzled_animate(withDuration: duration, delay: delay, options: options, animations: animations, completion: completion) } // インスタンスメソッドを上書き @objc func swizzled_addSubview(_ view: UIView) { print("\(type(of: self)):\(#function):\(#line), \( view )") swizzled_addSubview(view) } // プロパティ getter を上書き @objc func swizzled_get_frame() -> CGRect { let frame = swizzled_get_frame() print("\(type(of: self)):\(#function):\(#line), \( frame )") return frame } // プロパティ setter を上書き @objc func swizzled_set_frame(_ frame: CGRect) { print("\(type(of: self)):\(#function):\(#line), \( frame )") swizzled_set_frame(frame) } // 新しいプロパティを追加 private static var swizzleKeyName = "name" var swizzledName: String? { get { objc_getAssociatedObject(self, &UIView.swizzleKeyName) as? String } set { objc_setAssociatedObject(self, &UIView.swizzleKeyName, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } }イニシャライザとクラスメソッドは一部のクラスで指定方法に違いがあるようです。
有効化
上記
UIView+Swizzle.swift
をプロジェクト内に置き、AppDelegateのapplication(_:didFinishLaunchingWithOptions:)
で呼び出すだけです。func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { UIView.swizzle() ... return true }