【SwiftUI】SwiftUIのTIPS集:Part1

2021-12-29

前置き

お疲れ様です。
SwiftUIを書き始めているんですが、まぁ勝手が分からず1画面実装するにもggrggrggrggr....
XcodeよりもGoogleと睨めっこしている時間の方が長いのでは?となっている私です。

とりあえず気になるところはメモりながらやってましたんで、良いタイミングで投稿していこうかなぁと思っています。
以下に書いているものは1画面実装するにあたって調べた軌跡、、、まぁ初回なので、、、
クセというかTipsというか、こういう書き方するのねってものもちょいちょい書いているので参考にしてください。

タイトルをPart1 としているので、もしかしたらまた溜まってきたら投稿するかもね。
ちょっと調べりゃ解決するようなこともありますが、まぁ備忘録なのでいいでしょう。
ここを見に来る人も、どちらかというとこの中のどれかを引っ掛けて来るでしょうし。

Wrapper

SwiftUIの本体。ここでは特に説明しません。

@ObservableObject

  • オブジェクトを管理対象にする
  • 監視するプロパティは@Publishedを付与
  • Publishedを付与されたプロパティがデータであれば内包されている物も管理対象となる

@ObservedObject

  • 参照先で付与することにより監視が可能になる
  • 親Viewが再描写された場合は初期化される

@StateObject

  • 参照先で付与することにより監視が可能になる
  • 親Viewが再描写された場合も値を保持する

ex )

class ViewModel: ObservableObject {
    @Published var name: String
}

struct SampleView: View {
    @ObservedObject var viewModel = ViewModel()
}

Modifiers etc

とりあえず調べた後に、これから書いていく上でも覚えとかないといけなそうと感じたものだけピックアップしてメモってます。

GeometryReader

  • 画面のサイズ欲しい時はbody配下でないと取得できない

.frame

  • サイズ指定する場合は .background の前に配置しないと機能しない

.navigationTitle

  • navigationの設定系はターゲットとなるview配下で設定しないと機能しない?

ForEach(_, id: \.self)

forEachIdentifiable に準拠する必要があるが、プリミティブ型(String, intとか)はできないのでこれをつける必要がある。Identifiable が準拠された構造体等は不要。

Tips

print

View内では直で書けないっぽいので、こんな感じに書く。

let _ = print("hi!")

超簡易的なLoggerを作って差し込んでみている。

public class Logger {
    public static func output(_ text: String? = nil) {
        let _ = print("👮🏻‍♀️:  \(#function) \(String(describing: text))")
    }
}

body内でcomputed Propertyを使いたい

var arrayA = ["a", "b","c"]
var arrayB = ["d", "e","f"]

lazy var dataList: [String] = {
    var new = arrayA + arrayB
    return new
}()

dataListbody 内で使おうとするとlazy使えないとか色々怒られる。
定義してる property を利用しない computed なら @State var dataList で可能だが、ワイは使いたいんじゃって時はどうするか。

func createDateList() -> [String] {
    var new = arrayA + arrayB
    return new
}

関数にする。これで body 内でも使える。
他にいい方法があるかもしれないがこれしか今は知らない。

cornerRadiusを部分的に適用

現状は引数を与える or 別途Mask をかける等のプロパティはないっぽいので、自作拡張する必要がありそう。

struct RoundedCorner: Shape {
    var radius: CGFloat = .infinity
    var corners: UIRectCorner = .allCorners

    func path(in rect: CGRect) -> Path {
        let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        return Path(path.cgPath)
    }
}

extension View {
    func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
        clipShape(RoundedCorner(radius: radius, corners: corners))
    }
}


.cornerRadius(20, corners: [.topLeft, .bottomRight])

ちなみに .cornerRadius の適用は通常プロパティ含みforegorund / background の後じゃないと切り取られない。

propertyを条件分岐で出し分けたい

多分現状オブジェクトの出しわけしかできない。
特定のプロパティのみ条件分岐させたいときは拡張するしかなさそう。

extension View {
    @ViewBuilder
    func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
        if condition { transform(self) }
        else { self }
    }
}

Text("Hello World")
    .if(shouldBeRed) { $0.foregroundColor(.red) }

しかし副作用もあるようなので、素直にフラグ等で大元のオブジェクトの出し分けをしておいた方が良さそう。
もうちょっとフレキシブルにならないものか、、
https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/

forEachでプリミティブ型の値を重複表示したい時

基本 ForEach の利用では identifiable に準拠した値しか使えず、プリミティブ型には id: \.self を付与しないといけないが、これだと文字列や数値など重複する場合に重複表示できない。
その場合はStruct等でラップしてユニーク値を持たせてあげないといけない模様。

struct StringType: Identifiable {
    let id = UUID()
    let value: String
}

let strings = [StringType(value: "a"), StringType(value: "b")]
ForEach(strings) { data in
    Text(data.value)
}

ForEachでindexをvalueと同時に利用したい

swift同様 .enumerated() を利用可能。

ForEach(Array(list.enumerated()), id: \.offset) { index, item in
    ...
}

LazyVGrid / LazyHGrid の要素を幅いっぱいにする

そのまま組むと若干 padding が設定されてしまうっぽいので、明示的に

GridItem(spacing: 0)

を指定してあげる。
ちなみに LazyVGrid/LazyHGrid の方ではなくアイテムの方で指定してあげないとマージンは消えないようだった。

optionalの@stateをアンラップして渡す

Viewに親が保持している @State var hoge: String? をアンラップして渡したい。
こんなのが考えられる。

@State var value: String?
...
Hoge(value: ($value)!)

ビルドは通るが、どこかで valiue = nil などをした場合、更新順序の関係でクラッシュすることがあるので使わない方がよさそう。

これもダメ。

if let item = Binding($value) {
    Hoge(value: item)
}

mapでのアンラップなら行けた。

value.map { Hoge(value: Binding.constant($0)) }

なんか面倒ですね、ググると拡張作ったり色々やってる人がいますが、スマートに扱えないってことはアンチパターンなんですかね。
var cache: String? みたいにオプショナルにして一時キャッシュ的な役割持たせたりしたかったんですが、、

HStack/VStackでButtonを羅列した時にタップで全てのボタンが反応する

こんな感じでStackの中に複数のボタンを配置した場合、該当のボタンだけ反応して欲しいのに他のButtonも反応してしまう。

| Button | Button | ... | Button |

ググるとListの場合は .buttonStyle(DefaultValue) で解決できると散見できたが、当方Stackでの検証。

バージョンが多分違うので名前は変わってるかもしれないが、

HStack {
    Button {}
    ...
    Button{}
}
.buttonStyle(.borderless)

で個別の反応が確認できた。
まとめて記述したければ親につければよし、個別設定したければそれぞれのButtonにつければよし。

ちゃんと検証していないが、  .buttonStyle(.plain) かと思ったら反応したりしないのがあったのでこれじゃないっぽい?(.automatic というのもあり節目にdefaultとあったのでこっちが初期値で入ってるのかな)

テクニック的に解決するのであれば

HStack {
    Image(systemName: "")
        .onTapGesture {
            // do something
        }

    Rectangle()
        .onTapGesture {
            // do something
        }
}

のようにButton以外の他の View を配置し、 .onTapgesture を利用することでタップイベントを付与できる。

Error

Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'xxx' conform to 'Identifiable'

ForEach等の繰り返し処理で使う値はIdentifiableに準拠しなはれ。
ForEachでenumを使うときはこんな感じ(enumの意味合いから違った使い方だが繰り返したい場合もある)

enum Week: String, CaseIterable, Identifiable {
    case sun, mon, tue, wed, thu, fri, sat

    var id: String { return rawValue }
    var value: String {
        switch self {
        case .sun: return R.string.localizable.weekSun()
        case .mon: return R.string.localizable.weekMon()
        case .tue: return R.string.localizable.weekTue()
        case .wed: return R.string.localizable.weekWed()
        case .thu: return R.string.localizable.weekThu()
        case .fri: return R.string.localizable.weekFri()
        case .sat: return R.string.localizable.weekSat()
        }
    }
}

疑問点

  • @StateObject / @ObservedObjectの違いは理屈上はわかるが使いわけはまだわからんなぁ
  • 動的リストデータの組み込み先を@ViewBuilderなどに任せる場合、その内部でTextFieldのような引数に@Bindingを求められる場合のデータ参照方法(解決方法がわからんかったので、とりあえずリスト内で$dataを読むようにワークアラウンド)
  • Buttonにて、条件分岐などでImageを扱わない場合の空Viewは何がセオリー?EmptyViewだとインタラクションが効かなくなるので、とりあえずText("")を利用している。

MEMO

  • レイアウトはVStack HStack ZStackで色々組めるが、スクロールが想定される場合などはTableView = List, CollectionView = Collectionを利用した方がいい。Stackを用いるとScrollViewを明示的に実装しないといけないし、alignmentも整頓しないといけない。Rowの設定は内包するViewの方にmodifierをつける。
    あとこの場合、データ量が多い場合(画面に収まりきらないなど)は通常のVStackではなくLazyVStackを使う。
    通常Stackは全部読み込んでしまうので、データ量が多いと処理が重くなる。

終わり

疑問点とMEMOも書きっぱなしですが、まぁ良いでしょう!
癖っぽいのはわかってきたぞ。
アーキテクチャはMVVMが無難かしらね、TCAは最近下火なの?大体Reduxに近かったので大枠読んでデプロイしてないけど、うーんこのまま盛り上がらないなら今すぐ進める必要もないかなぁ。
PresenterはMVVMであとはUsecase/Repository/Searvice等でディレクトリ切れば、まぁいい感じに保守は出来そう。

とりあえずPart1は以上です!