SwiftUI 半分サイズのモーダルシート PartialSheet

iOSのアプリでよく見かける画面サイズの半分ぐらいで下から出てくるモーダルシートですが、SwiftUIでどのように実装するのか標準のsheetでは思った挙動ではないようなので、調べてみるとPartialSheetを使うと実現できました。

PartialSheetは下のURL(GitHub)です。

https://github.com/AndreaMiotto/PartialSheet

XcodeのAccounts画面でGitHub登録し、メニュー[File|Swift Packages|Add Package Dependency…]を選択しダイアログからPartialSheetを使いたいProjectを選びます。

Choose Package Repository]画面検索用フィールドに「PartialSheet」と入力します。

Next]ボタンを押すとProjectに追加されます。

ソースコードに「import PartialSheet」すると使うことができます。@EnvironmentObjectPartialSheetManagerを設定しないといけない。

@EnvironmentObject var partialSheetManager: PartialSheetManager

半モーダルシートはどこかのActionで下記のように記述します。

self.partialSheetManager.showPartialSheet(content: {
	VStack {
		Text("")
	}.frame(height: 120)
})

PartialSheetは、NavigationViewの中でないと思ったような動きをしてくれませんでした。

NavigationView{
}.addPartialSheet()

NavigationViewに.addPartialSheet()を設定します。ためしたコード全体は下記のような感じです。

import PartialSheet

class TComponent : ObservableObject {
    @Published var textValue : String = "Show HalfSheet"
    static func resourcesLoadImage(name: String) -> CGImage {
        guard
            let url = Bundle.main.url(forResource: name, withExtension: "png"),
            let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
            let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
        else {
            fatalError("Couldn't load image \(name) .")
        }
        return image
    }
}

struct HalfSheet: View {
    @EnvironmentObject var Component1: TComponent
    @EnvironmentObject var partialSheetManager: PartialSheetManager

    var body: some View {
        NavigationView{
            VStack {
                Text(self.Component1.textValue)
                Button("初代", action: {
                    self.Component1.textValue = "初代"
                    self.partialSheetManager.showPartialSheet(content: {
                        VStack {
                            Image(TComponent.resourcesLoadImage(name: "shodai"), scale: 3, label: Text("") ).clipShape(Circle())
                                .overlay(Circle().stroke(Color.red, lineWidth: 4))
                            Text("ウルトラマン")
                        }.frame(height: 120)
                    })
                })
                Button("帰ってきた", action: {
                    self.Component1.textValue = "ジャック"
                    self.partialSheetManager.showPartialSheet(content: {
                        VStack {
                            Image(TComponent.resourcesLoadImage(name: "shinman"), scale: 2, label: Text("") ).clipShape(Circle())
                                .overlay(Circle().stroke(Color.blue, lineWidth: 4))
                            Text("ジャック")
                        }.frame(height: 200)
                    })
                })
            }
        }.addPartialSheet()
    }
}

上記コードでは、2つshowPartialSheet()を作っています。Imageは少し面倒なコードを書いていますがResourcesディレクトリにPNGファイルを入れていたためです。

Assets.xcassetsに画像を入れた場合は、Image(“shodai”)でOKです。

struct HalfSheet_Previews: PreviewProvider {
    static var previews: some View {
        HalfSheet().environmentObject(TComponent()).environmentObject(PartialSheetManager())
    }
}

Viewを使う場合.environmentObject()を2つ設定すればうまく動きました。

SwiftUI 簡易的な Active Indicatorを作る

SwiftUIの部品にはインジケータがないようなのでCircleを使って簡易的にインジケータを作成します。コードを書く量は少しです。

struct ActiveIndicatorOne: View {
    @State var anim = false
    var body: some View {
        Circle().trim(from: 0, to: 0.7)
            .stroke(AngularGradient(gradient:
                .init(colors: [Color.blue, Color.blue.opacity(0.1)]), center: .center),
                    style: StrokeStyle(lineWidth: 8.0, lineCap: .round) )
            .rotationEffect(Angle(degrees: anim ? 360 : 0 ))
            .animation(Animation.linear(duration: 0.5).repeatForever(autoreverses: false))
            .onAppear(){
                self.anim.toggle()
                
        }
        .frame(width: 50, height: 50)
    }
}

これだけです。

Circleを使ったActiveIndicator
Circleを使ったActiveIndicator

SwiftUI JSON文字列をJSONDecoderで構造化する。

SwiftUIで、Stringに入っているJSONを処理する場合JSONDecoderで構造体にdecodeして利用します。Swiftコード内に下記のJSON文字列を用意します。

let timeTraveller_json:String = """
	{ "morlock" :
		{"Eloi":"🦄 Weena 🌸"
	}
"""

上のJSON文字列と同じような構造体を作成します。

struct TTimeTraveller : Decodable {
    struct TMorlock : Decodable {
        var Eloi: String?
    }
    var morlock : TMorlock
}

構造体の派生元はDecodableにする必要があるようでした。この例ではTMorlockTTimeTravellerの中に書きましたが分けても問題ないようです。

Viewを作成し、Button アクションの中に「“Eloi”:”🦄 Weena 🌸”」を呼び出せるようにし、Text表示させます。

struct StringJSON: View {
    let timeTraveller_json:String = """
        { "morlock" :
            {"Eloi":"🦄 Weena 🌸", "JapaneseName": "ウィーナ"}
        }
    """
    @State var s1: String = ""
    var body: some View {
        VStack{
            Text(self.s1)
            Button("Button1", action: {
                do {
                    let model1 = try JSONDecoder().decode(TTimeTraveller.self, from: self.timeTraveller_json.data(using: .utf8)! )
                    self.s1 = model1.morlock.Eloi!
                } catch { }
            })
            Spacer()
        }
    }
}

JSON文字列に「“JapaneseName”: “ウィーナ”」を追加しました。使わなければ問題ありません。その反対の構造体側に余計な項目があっても使わない場合エラーにはなりませんでした。

struct TMorlock : Decodable {
    var Eloi: String?
    var JapaneseName: String?
}

TMorlockJapaneseNameを追加すればその項目も利用できます。

JSON文字列を構造体にdecode
JSON文字列を構造体にdecode