20200526のSwiftに関する記事は15件です。

【Swift】TableViewでカスタムセルを作成した場合

個別にセル(xib)を作っている場合、以下のコードで紐付けする。

 //        個別に作成したセル(CustomCell)と紐づける
    tableview.register(UINib(nibName: “カスタムセルのファイル名", bundle: nil), forCellReuseIdentifier: "ID")

}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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に記述していく

コンポーネントを図にすると次のようになるでしょう。

Composable_Archtecture.png

良い点

良い点も超ざっくり説明すると、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になる。

_2020-05-26_20.48.33.png

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がわかりやすい。

_2020-05-26_20.53.37 1.png

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にするという例となっている。

スクリーンショット 2020-05-26 20.54.44.png

その他

複数stateを階層化できる?

  • 配列としてStateを保持していくこともできる
    • 並列にNestedStateを持てるし(初期画面に3つ並べる)
      • 一つのStateを掘り下げていくこともできる
    • サンプル
      • Recursive state and actions

_2020-05-26_20.47.00.png

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を使うのは内部の値を気にせず型を作る時、それを比較するようにするのに使うわけです。

内部をカラにすることでそのカラのインスタンスだけで比較することができるというのは理にかなっていて興味深いですよね。

おわりに

Composable ArchitectureはSwiftUI専用ってわけではないですが、SwiftUIを使ってアプリを開発する上でどのような構成にするのかという一つの指針になるはずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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に記述していく

コンポーネントを図にしてみると次のような感じです。

Composable_Archtecture.png

公式の作画ではなく私の作画なので間違いあるかもしれません。その場合はコメントなどをください。

良い点

良い点も超ざっくり説明すると、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があったとします

_2020-05-26_20.48.33.png

その画面を示す大外の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がわかりやすい。

_2020-05-26_20.53.37 1.png

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にするという例となっている。

スクリーンショット 2020-05-26 20.54.44.png

その他

複数stateを階層化できる?

  • 配列としてStateを保持していくこともできる
    • 並列にNestedStateを持てるし(初期画面に3つ並べる)
      • 一つのStateを掘り下げていくこともできる
    • サンプル
      • Recursive state and actions

_2020-05-26_20.47.00.png

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を使うのは内部の値を気にせず型を作る時、それを比較するようにするのに使うわけです。

内部をカラにすることでそのカラのインスタンスだけで比較することができるというのは理にかなっていて興味深いですよね。

おわりに

Composable ArchitectureはSwiftUI専用ってわけではないですが、SwiftUIを使ってアプリを開発する上でどのような構成にするのかという一つの指針になるはずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【プログラミング初心者】Swift基礎~構造体・クラス~

型を定義する

以前Swiftにおける基本となる型について説明しました。
この基本型とif文とfor文さえあればプログラミングはできるのですが、これだけだとプログラムは煩雑になり管理しづらくなります。
そこである1つのデータや処理のまとまりを新しい型として定義することで全体の見通しをよくすることができます。
その型定義の方法に構造体クラスという手法があります。

構造体

構造体の定義

構造体を定義するための構文は以下となります。

struct 型名 {
   ...
}

もう少し具体的な実装をします。会社の構造を例にしてみましょう。
会社を表す型としてCompanyという構造体を作成します。
Company.swiftというファイルを作成し、構造体を定義します。

Company.swift
struct Company {
}

これでCompanyという構造体が定義されました。
実際にViewController.swiftなどから使ってあげましょう。
Intの実態は数値なので変数には数値を代入すればよかったのですが、構造体の実態はオブジェクトと呼ばれるデータのまとまりになります。
そのためIntとは少し違い作成する構文は構造体名()でオブジェクトを作成します。

ViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()
    var company: Company = Company()
}

メンバ変数

さらにここからCompanyを表す属性を定義していきます。
会社を表すためにはどんな項目が必要でしょうか?
まず会社名、社長、あとは売り上げあたりを定義してみましょう。

Company.swift
struct 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.swift
override func viewDidLoad() {
    super.viewDidLoad()
    var company: Company = Company(name: "株式会社山田商事",
                                   president: "山田太郎",
                                   earnings: 1000000)
}

さてこれで「株式会社山田商事」という会社が作られました。
メンバ変数を参照・更新してみましょう。
メンバへアクセスする構文は変数.メンバです。

ViewController.swift
var 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.swift
struct Department {
    // 部署名
    var name: String
    // 部長
    var manager: String
}
Company.swift
struct 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.swift
struct 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を使って宣言されていなければならないというルールがあります。
配列appendremoveと同じです。
少し脱線しますが実は配列も構造体として定義されています。
appendなどの定義を見るとmutatingがついていることを確認できます。
配列構造体は配列自体はメンバ変数として保持しています。そのためappendを呼び出すとメンバ変数が更新されるためmutatingとして定義されているというわけです。

さてそれでは定義したメソッドを呼び出してみましょう。

ViewController.swift
var 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.swift
print(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.swift
var 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.swift
mutating 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.swift
var 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.swift
class Human {

}
ViewController.swift
var human = Human()

これで人間のオブジェクトを作成することができました。

クラスから作成されたオブジェクトのことは特にインスタンスと呼びます。
ですが経験上あまり明確に区別して話す機会はないように思うので基本的にはオブジェクトと言っておけば問題ないでしょう。

一応オブジェクト、インスタンスそれぞれの意味は

  • オブジェクト
    • プログラム上で扱うデータ全般。その実体化したデータを1つのモノとして見る考え方。
  • インスタンス
    • 何らかのクラスに基づいて実体化されたオブジェクト

となりますのでインスタンスもつまりはオブジェクトの一種です。
なのでインスタンスをオブジェクトと言っても誤りではないというわけです。

プロパティ/メソッド

クラスも構造体と同じようにメンバ変数とメソッドを持たせることができます。
厳密には違いますがクラスで定義したメンバ変数をプロパティと呼ぶととりあえずは覚えてください。

定義の仕方も同様です。
まずはプロパティを定義します。

Human.swift
class Human {
    // 名前
    var name: String
    //  年齢
    var age: Int
}

すると以下のようなエラーが発生します。

Class 'Human' has no initializers

「イニシャライザがありません」というようなエラーです。
イニシャライザとは初期化の際に使用する、少し特殊なメソッドです
別の言語ではコンストラクタといい、つまりクラスを構成する人というイメージです。

構造体のときはこのエラーは発生しませんでしたが実は構造体は暗黙的にイニシャライザを自動で持ってくれます。
ですがクラスの場合は明示的に作ってあげる必要があります。

イニシャライザの定義の仕方は色々ありますが今回は最も単純なイニシャライザを定義します。
基本構文はinit(引数)です。

Human.swift
class Human {
    // 名前
    var name: String
    //  年齢
    var age: Int

    /// 初期化
    /// - Parameters:
    ///   - name: 初期値となる名前
    ///   - age: 初期値となる年齢
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

呼び出し方はクラス名(引数)です。
あまりやりませんがクラス名.init(引数)でも呼び出せます。

ViewController.swift
var human = Human(name: "山田太郎", age: 25)
var humanInit = Human.init(name: "山田太郎", age: 25)

構造体のときと同じ生成の仕方ができました。
クラスも構造体もイニシャライザのみクラス名(引数)という呼び出し方ができます。

メソッドも構造体同様に定義でき、呼び出し方も同様です。

Human.swift
class 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.swift
var 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2変数、4分岐の if 文について

変数 min_value, max_value が存在するとき、それぞれの条件で処理を変えたい

例えば…

min_value max_value 処理
null null なにもしない
null not null 上限より小さいことを検証
not null null 下限より大きいことを検証
not null not null 範囲内にあることを検証

普通はこんな感じ

Main.java
class 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.swift
func 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.py
def 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)]()

もっと良い方法がありましたら、教えていただきたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIButtonに隠しActionをつける方法

はじめに

ただどう動くんだろう?って言うだけの話で実用性は皆無です:innocent:
なんか UIButton に謎のアクションを付けれたというお話です。

環境

  • Xcode 11.4
  • iPhone 11 Pro Max (13.4)

実装

  1. StoryboardでFirstViewControllerとSecondViewControllerを用意する
  2. FirstVCにボタンをのせる
  3. IBActionでボタンのアクションをFirstVCに紐付ける(tappedFirst(_ sender: Any)
  4. SecondVCにFirstVCのボタンをコピペする
  5. IBActionでボタンのアクションをSecondVCに紐付ける(tappedSecond(_ sender: Any)

こんな感じ

storyboard

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 のボタンを確認すると...

こうなる!

button_action

tappedFirst がついてる。シュミレータで SecondVC のボタンを押下してみると Second と print 表示される。(FirstVCの tappedFirst(_) が呼ばれたりはしない)unrecognized selector でクラッシュもしない:clap:

調査

下記のようにボタンのカスタムクラスを作り 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表示時のログ

addTarget

FirstVCのボタン押下時のログ

beginTracking
endTracking
sendAction
First
addTarget

最後の addTarget は SecondVC 表示時のやつ

SecondVC表示時のログ

addTarget

アクションは2つあるけど1回しか呼ばれない:interrobang:

SecondVCのボタン押下時のログ

beginTracking
endTracking
sendAction
Second

調査結果

SecondVC のボタンの addTarget が一回しか呼ばれていないことからそもそも tappedFirst のアクションは追加されていないみたい:thinking:

button_action

Storyboardでアクションを付けたボタンを他画面からコピペすれば動作しない謎のアクションを付けることができる!これをすれば他の開発者を惑わすこともたやすいだろう:sunglasses:

さいごに

実装中に謎のアクションをみつけたので調べてみた結果です。活かせる場はあるだろうか:smiling_imp:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Realmエラー] Value of type 'Realm' has no member 'objectForPrimaryKey' 解決方法

修正前

a.swift
if let data = realm.objectForPrimaryKey(Seismograph.self, key: _markerId) {
                    print("id:1 \(data.???)")
                }

修正後

a.swift
if let data = realm.object(ofType: Seismograph.self, forPrimaryKey: _markerId) {
                    print("id:1 \(data.???)")
                }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift5][ FAB]フローティングアクションボタンの作り方

フローティングアクションボタンの作り方。

実装イメージ

TableViewCellの上に浮いてるデザインで、スライドしても固定されるように実装します。
スクリーンショット 2020-05-26 15.49.12.png

Storyboard

まずbuttonのみを置いてAutoLayoutをつけます。
スクリーンショット 2020-05-26 15.54.30.png
スクリーンショット 2020-05-26 15.32.38.png
その上にTableView,TableViewCellを画面一杯に配置します。TableViewCellのIdentifierはCellに設定しましょう。
スクリーンショット 2020-05-26 16.11.52.png

ソースコード

ViewController.Swift
import 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
    }


}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift5][FAB]フローティングアクションボタンの作り方

フローティングアクションボタンの作り方。

実装イメージ

TableViewCellの上に浮いてるデザインで、スライドしても固定されるように実装します。
スクリーンショット 2020-05-26 15.49.12.png

Storyboard

まずbuttonのみを置いてAutoLayoutをつけます。
スクリーンショット 2020-05-26 15.54.30.png
スクリーンショット 2020-05-26 15.32.38.png
その上にTableView,TableViewCellを画面一杯に配置します。TableViewCellのIdentifierはCellに設定しましょう。
スクリーンショット 2020-05-26 16.11.52.png

ソースコード

ViewController.Swift
import 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
    }


}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift5] TextFieldのあるAlertを表示させる方法

SwiftでTextFieldのあるAlertを表示させる方法。

実装イメージ

このようなタイトル、サブタイトル、入力欄、キャンセル、追加で構成されたAlertを作成して行きます。
スクリーンショット 2020-05-26 14.57.09.png

Storyboard

buttonを押すとAlertが表示されるようにするため、好きな場所にbuttonを1つ追加しましょう。
スクリーンショット 2020-05-26 14.19.46.png

ソースコード

ViewController.Swift
import 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押してくださると僕がめっちゃ喜びます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

スクリーンショット 2020-05-26 13.45.07.png

一部角丸ができるカスタム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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

スクリーンショット 2020-05-26 13.45.07.png

一部角丸ができるカスタム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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIでNaivigationViewが重なったら

起きたこと

NavigationViewが重なっちゃった!
スクリーンショット 2020-05-26 13.41.42.jpg

原因と対処法

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

こんなソースコードはイヤだ-ifの条件が逆じゃない?

プログラムのソースコードのより良い書き方をまとめていこうと思います。

ifの条件が逆じゃない?

sample.swift
  if (error != nil) {
    print("エラーが発生")
  } else {
    print("正常処理")
  }

どのようにリファクタリングできるのか

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む