しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

SwiftUIの@Stateや@AppStorageのようなものを自作する

タイトルの通り、@XXXを自作してみました

この機能は@propertyWrapperを使う事で自作が可能です

下は値をセットした時に新しい値をprintするだけのものになります

@propertyWrapper
class DebugValue<Value> {
    var value: Value
    
    init(wrappedValue: Value) {
        value = wrappedValue
    }
    
    var wrappedValue: Value {
        get {
            return value
        }
        set {
            print(newValue)
            value = newValue
        }
    }
}

使い方は下の通りです
ただ@Stateを使っていないので"count:(count)"の部分は自動で更新されません

struct ContentView: View {
    @DebugValue var count = 0
    
    var body: some View {
        VStack {
            Button("count:\(count)", action: {
                count += 1
            })
        }
        .padding()
    }
}

変更にViewに反映したい場合はDynamicPropertyに準拠してプロパティも@Stateで持つようにします

@propertyWrapper
struct DebugValue<Value>: DynamicProperty {
    @State private var value: Value
    
    init(wrappedValue: Value) {
        value = wrappedValue
    }
    
    var wrappedValue: Value {
        get {
            return value
        }
        nonmutating set {
            print(newValue)
            value = newValue
        }
    }
}

下のようにInt型に固定したり初期値をセットする事も可能です

@propertyWrapper
struct DebugValue: DynamicProperty {
    @State private var value = 0
    
    var wrappedValue: Int {
        get {
            return value
        }
        nonmutating set {
            print(newValue)
            value = newValue
        }
    }
}

View側では下のように使います

struct ContentView: View {
    @DebugValue var count
    
    var body: some View {
        VStack {
            Button("count:\(count)", action: {
                count += 1
            })
        }
        .padding()
    }
}

@AppStorageのようにUserDefaultsに保存したい場合は下のようにします

@propertyWrapper
struct DebugValue<Value>: DynamicProperty {
    @State private var value: Value
    private let key: String
    
    init(wrappedValue: Value, _ key: String) {
        value = UserDefaults.standard.object(forKey: key) as? Value ?? wrappedValue
        self.key = key
    }
    
    var wrappedValue: Value {
        get {
            return value
        }
        nonmutating set {
            print(newValue)
            value = newValue
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

StoreKit2での購入処理を試してみる

StoreKit2を使った購入処理を試してみました

商品情報の取得は下の通りです

Task {
     let products = try? await Product.products(for: ["com.example"])
    products?.forEach {
        print($0)
    }
}

実行すると下の形で商品情報を取得できます

{
  "attributes" : {
    "description" : {
      "standard" : "説明文"
    },
    "editorialArtwork" : {

    },
    "icuLocale" : "ja_JP@currency=JPY",
    "isFamilyShareable" : 0,
    "isMerchandisedEnabled" : 0,
    "isMerchandisedVisibleByDefault" : 0,
    "isSubscription" : 0,
    "kind" : "Non-Consumable",
    "name" : "サンプル",
    "offerName" : "com.example",
    "offers" : [
      {
        "assets" : [

        ],
        "buyParams" : "productType=A&price=300000&salableAdamId=XXXX&pricingParameters=STDQ&pg=default&offerName=com.example&appAdamId=XXXX",
        "currencyCode" : "JPY",
        "price" : 300,
        "priceFormatted" : "¥300",
        "priceString" : "300",
        "type" : "buy"
      }
    ],
    "releaseDate" : "2009-06-17",
    "supportsRuntimePricing" : 0,
    "url" : "https:\/\/sandbox.itunes.apple.com\/jp\/app\/XXXX/idXXXX"
  },
  "href" : "\/v1\/catalog\/jp\/in-apps\/XXXXXX?l=ja",
  "id" : "1197842797",
  "type" : "in-apps"
}

購入処理は下の通りです

let result = try? await product.purchase()
print(result)

購入結果は下のように取得できるので処理を書いていきます

switch result {
case .success(let verificationResult):
    switch verificationResult {
    case .unverified(let transaction, let error): break
    case .verified(let transaction):
        await transaction.finish()
}
case .userCancelled: break
case .pending: break
@unknown default: break

過去の購入履歴は下のように取得できます

let transactions = Transaction.currentEntitlements
for await transation in transactions {
    print(transation)
}

iOS26のglassEffectを試してみる

iOS26でglassEffectというAPIを追加したので試してみました
こちらはSwiftUI専用のAPIのようで、UIKitではUIBlurEffectなどを使う事で似た状態を再現できます

まずは下のように画像とテキストが重なった状態にします

struct ContentView: View {
    var body: some View {
        ZStack {
            Image(.sample)
            Text("Hello, world!")
        }
    }
}

アプリを起動すると下のような画面になります

Textを下のように全画面にしてglassEffectを追加します

Text("Hello, world!").frame(maxWidth: .infinity, maxHeight: .infinity).glassEffect()

実行すると下のような表示になります

glassEffectはtintで色を付ける事ができます

glassEffect(.regular.tint(.blue.opacity(0.3)))

実行結果は下の通りです

第一引数をデフォルトのregularからclearにするとぼかしのかからないガラスになります
あとはidentityというglassEffectを無効化するものもあります

glassEffect(.clear)
glassEffect(.identity)

clearを選ぶと下のような表示になります
分かりにくいですが元画像より少しぼやけている感じになります

glassEffectは角丸がかかるのですが、第二引数にRectangleを渡すことで角丸をなくす事ができます

glassEffect(in: Rectangle())

結果は下の通りです

Run Scriptでoutputsがないという警告の対応をする

Xcodeで下のような警告が出ていたので調べみました

Showing Recent Issues Run script build phase 'Run Script(xassets)' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase.

こちらはRun scriptのOutput Filesを指定していない事が原因です
Run scriptは入力ファイルと出力ファイルを見て実行するかどうか決定するのですが、ファイルを指定していない場合は警告が出ます

対策としては、Output Filesの出力と指定をして毎回実行されないようにする事があります

もしOutput Filesを見ずに毎回実行して良い場合はBased on dependency analysisのチェックを外せばファイルを見ずに毎回実行するようになります
ただチェックを外した場合は下のような警告が出るようになります

Showing Recent Issues Run script build phase 'Run Script' will be run during every build because the option to run the script phase "Based on dependency analysis" is unchecked.

Output Filesはないけど毎回実行してほしい時はFor install builds onlyのチェックを入れます
これを使うとアーカイブやインストールの時にだけ実行してくれるようになります

@unknown defaultの動作確認

Swift6から@unknown defaultが必須になったので調査しました

下のようなswitch文ですがSwift6からはSwitch covers known cases, but 'UIUserInterfaceStyle' may have additional unknown values, possibly added in future versionsというエラーになります

switch view.window!.windowScene!.screen.traitCollection.userInterfaceStyle {
case .unspecified: print("unspecified")
case .dark: print("dark")
case .light: print("light")
}

こちらは下のcaseを追加する事で解消します

@unknown default: print("@unknown default")

@unknown defaultについてですが、将来的にcaseが追加された時のための記述になります
将来OS側で.light、.darkに代わるモードが出た時も@unknown defaultがあれば対応できる想定です

@unknown defaultですが、これがある場合にcaseを省略するとSwitch must be exhaustiveという警告になります

switch view.window!.windowScene!.screen.traitCollection.userInterfaceStyle {
case .unspecified: print("unspecified")
case .dark: print("dark")
@unknown default: print("@unknown default")
}

defaultがある場合は@unknown defaultは不要です、逆に@unknown defaultを入れるとエラーになります

switch view.window!.windowScene!.screen.traitCollection.userInterfaceStyle {
case .unspecified: print("unspecified")
case .dark: print("dark")
default: print("default")
}

自分で定義したenumでは@unknown defaultは不要です

enum Test {
    case a
    case b
    case c
}

let test = Test.a
switch test {
case .a: print("a")
case .b: print("b")
case .c: print("c")
}

外部ライブラリも下のように@frozenが付いている場合は@unknown defaultは入れなくて問題ありません

@frozen public enum Test {
    case a
    case b
    case c
}

【UIKit】iOS26の画面真ん中スワイプで前画面に戻るので防ぐ

タイトルの通り、iOS26では画面の端以外をスワイプした場合も前の画面に戻る仕様になりました これを防ぎたい場合、下のようにinteractiveContentPopGestureRecognizerを無効にする必要があります

if #available(iOS 26.0, *) {
    navigationController?.interactiveContentPopGestureRecognizer?.isEnabled = false
}

iOS26で登場したAlarmKitを試してみる

iOS26でAlarmKitという新しい通知が出たので試してみました
こちらの通知ですとおやすみモードでも通知を鳴らす事ができます

まずはInfo.plistにNSAlarmKitUsageDescriptionキーと利用理由を追加します

次は下のようにrequestAuthorizationで通知許可を求めます

import AlarmKit

Task {
    let status = try? await AlarmManager.shared.requestAuthorization()
    print(status)
}

requestAuthorizationを呼ぶと下のようなアラートが表示されます

アラームのセット方法は下の通りです
利用するクラス数が多いので慣れるまで大変そうでした

import AlarmKit
struct MyMetadata: AlarmMetadata {
}

let scheduleFixed = Alarm.Schedule.fixed(Date(timeIntervalSinceNow: 5))
let alarmButton = AlarmButton(text: "button", textColor: .blue, systemImageName: "stop")
let alert = AlarmPresentation.Alert(title: "テスト", stopButton: alarmButton)
let alarmPresentation = AlarmPresentation(alert: alert)
let attributes = AlarmAttributes<MyMetadata>(presentation: alarmPresentation, tintColor: .green)
let alarmConfiguration = AlarmManager.AlarmConfiguration<MyMetadata>(schedule: scheduleFixed, attributes: attributes)
Task {
    try? await AlarmManager.shared.schedule(id: UUID(), configuration: alarmConfiguration)
}

アラームが鳴った様子は下の通りです
今回の実装ではiPhoneのタイマーと同じようにストップするまで鳴り続けました
iOS26.1で試した所、おやすみモードでも音付きで鳴る事が確認できました

今回は普通のアラームだけ試したのですが、カウントダウン方式やスヌーズ設定など様々な事ができるようです