Open17

My Tips

平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

クラスメソッドとスタティックメソッドの違い

クラスのインスタンスを必要とせず、共にクラス自身に紐付くメソッドではあるが、両者にはオーバーライド可能か否かという点で違いがある。クラスメソッドはオーバーライド可能、スタティックメソッドはオーバーライド不可である。

class Child {
    class func morningGreeting() {
        print("Good morning.")
    }
    
    static func nightGreeting() {
        print("Good evening.")
    }
}

class Parent: Child {
    // オーバーライド可能
    class override func morningGreeting() {
        print("Good morning!!!")
    }
    
    // オーバーライド不可(エラー発生)
    override static func nightGreeting() {
        print("Good evening!!!")
    }
}
平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

クラスと構造体の一意性について

クラスは明示的にIdentifiableに準拠させることで、自動的にidプロパティを持つ。ヒープ領域を使用する参照型の特徴によるものか。

class C: Identifiable {}

let c1 = C()
let c2 = C()

print(c1.id) // ObjectIdentifier(0x0000600000014550)
print(c2.id) // ObjectIdentifier(0x00006000000143a0)

構造体の場合、一意性を保証するにはHashableなidを持たせる必要がある。これによりIdentifiableプロトコルに準拠する。

struct S: Identifiable {
    let id = UUID()
}

let s1 = S()
let s2 = S()
print(s1) // S(id: 6865FA7D-AE99-4C20-951B-6664635F781B)
print(s2) // S(id: BF19C9D6-59B1-48DD-A06B-BD5D2A9307EC)
平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

The CFBundleVersion of an app extension ('1') must match that of its containing parent app ('2').

ウィジェットに対する警告。アプリターゲットのビルド番号とウィジェットターゲットのビルド番号が異なる際に表示される。

ウィジェットターゲットの「Build Settings > Versioning > Current Project Version」をアプリターゲットと同じ番号に設定する。

※ 念の為、アプリターゲットのバージョン番号とウィジェットのマーティング番号も揃える。

平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

NSLayoutConstraintによる警告の回避
UIConstraintBasedLayoutLogUnsatisfiableをfalseに設定する。

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear {
#if DEBUG
                    UserDefaults.standard.set(true, forKey: "_UIConstraintBasedLayoutLogUnsatisfiable")
#endif
                }
        }
    }
}

対症療法のような気もするが、なぜか下記のようなシンプルなものにも警告が出てくる。

TextField("Placeholder", text: $text)
    .toolbar {
        ToolbarItemGroup(placement: .keyboard) {
            Button("Button", action: {})
            Button("Button", action: {})
            Button("Button", action: {})
        }
    }
平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

Swift Packageの作成から使用まで

  1. GitHubでリモートリポジトリを作成
  2. 任意のディレクトリにクローン
  3. パッケージ作成:swift package init --type library -- name パッケージ名
(Xcodeで作成すると階層がめんどい。やってみりゃわかる。)
  4. Packageのイニシャライザの引数platformsに[SupportedPlatform.iOS(.v16), SupportedPlatform.macOS(.v13)]を指定(たぶんミニマムバージョン)。引数platformsはnameの次に記入。
  5. パッケージ名.swiftにシンボル(クラスや関数)を記入
※ イニシャライザにもpublicをつけることを忘れずに!
  6. commitを済ませた後にGitHubにpush
  7. GitHubにてReleaseを作成
「github.com/username/repositoryname」に「tag」ってのがある。そこで作成する。Releaseはタグに対してつけるタグ?みたいな感じで、タグがなきゃつけられない模様。
  8. 適当なプロジェクトを作成する
  9. 「Project > Package Dependencies」の「+」を押下し、GitHubのリポジトリURLを入力し、パッケージを追加
  10. ライブラリをインポートして使用する

参考
https://dev.classmethod.jp/articles/howto-create-original-swift-package/#toc-8
色々知れそう
https://qiita.com/uhooi/items/2f36b85f5f41cbd35189

平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

printChanges

Viewのbodyが再計算されたときに、どのプロパティが変更されたのかをコンソールに出力

import SwiftUI

struct ContentView: View {
    @State private var num = 10000
    
    var body: some View {
        let _ = Self._printChanges()

        Button {
            num = Int.random(in: 10000...99999)
        } label: {
            Text(String(num))
        }
        .buttonStyle(.borderedProminent)
    }
}

#Preview {
    ContentView()
}

// 出力
// ContentView: @self, @identity, _num changed.
// ContentView: _num changed.
// ContentView: _num changed.
// ContentView: _num changed.

平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

NavigationLinkの引数valueにnavigationDestination(for:destination:)で設定されていない型があってもコンパイルエラーが出ない

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("1", value: 1)
                NavigationLink("2", value: "2") // String
            }
            .navigationDestination(for: Int.self) { num in
                Text(num.description)
            }
        }
    }
}
平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

単純な変換をmapを用いて行う場合は以下の書き方が便利。Optional, Result, Arrayでも以下の書き方は使える。

func convertToKanji(from num: Int) -> String {
    return switch num {
    case 1: "一"
    case 2: "二"
    case 3: "三"
    default: "不明"
    }
}

let subject = PassthroughSubject<Int, Never>()
let subscription = subject
    .map(convertToKanji(from:))
//    .map({ num in
//        convertToKanji(from: num)
//    })
    .sink { str in
        print(str)
    }

subject.send(1)
subject.send(2)
subject.send(3)
subject.send(4)
平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

Modifier: toolbarTitleMenu

おそらくタイトルのディスプレイモードをインラインに指定しなければ機能しない。

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                Text("Text1")
                Text("Text2")
                Text("Text3")
            }
            .navigationTitle("Title")
            .navigationBarTitleDisplayMode(.inline)
            .toolbarTitleMenu {
                Button("Button1") { print("Button1 pushed") }
                Button("Button2") { print("Button2 pushed") }
                Button("Button3") { print("Button3 pushed") }
            }
        }
    }
}

#Preview {
    ContentView()
}
平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

Modifier: containerRelativeFrame

いつぞやに少々話題となっていたcontainerRelativeFrame。
最上位?の親Viewのサイズに応じたサイズを指定できる。要調査。

struct ContentView: View {
    var body: some View {
        NavigationStack {
            Text("25%")
                .foregroundStyle(.white)
                .frame(height: 100)
                .containerRelativeFrame(.horizontal) { length, _ in
                    return length * 0.25
                }
                .background(.pink)
            Text("50%")
                .frame(height: 100)
                .containerRelativeFrame(.horizontal) { length, _ in
                    return length * 0.5
                }
                .background(.blue)
            Text("75%")
                .frame(height: 100)
                .containerRelativeFrame(.horizontal) { length, _ in
                    return length * 0.75
                }
                .background(.green)
        }
        .font(.title2.bold())
        .foregroundStyle(.white)
    }
}
平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

SwiftUI ContextMenu

struct ContentView: View {
    var body: some View {
        Text("Hello, world.")
            .padding()
        
            // previewなし
            .contextMenu(ContextMenu(menuItems: {
                Button("Button1", action: { print("Button1 pushed") })
                Button("Button2", action: { print("Button2 pushed") })
                Button("Button3", action: { print("Button3 pushed") })
            }))
        
//            // previewあり
//            .contextMenu {
//                Button("Button1", action: { print("Button1 pushed") })
//                Button("Button2", action: { print("Button2 pushed") })
//                Button("Button3", action: { print("Button3 pushed") })
//            } preview: {
//                Text("This is preview.")
//                    .padding()
//            }

    }
}
平成n年生まれ@iOSエンジニア志望平成n年生まれ@iOSエンジニア志望

safeAreaPadding

iOS17以降
ScrollViewのStackに対してSafeAreaを付与している。

struct ContentView: View {
    var body: some View {
        VStack(spacing: 50.0) {
            ScrollView(.horizontal) {
                HStack(spacing: 10.0) {
                    ForEach(0..<10) { _ in
                        Text("safeAreaPadding適用前")
                    }
                }
            }
            .padding(.vertical)
            .border(.pink)
            ScrollView(.horizontal) {
                HStack(spacing: 10.0) {
                    ForEach(0..<10) { _ in
                        Text("safeAreaPadding適用前")
                    }
                }
            }
            .safeAreaPadding(.horizontal, 20.0)
            .padding(.vertical)
            .border(.blue)
        }
    }
}