1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIでダイアログ(UIコードの使い回し)

Last updated at Posted at 2023-11-15

今回はSwiftUIでダイアログを表示する話。
様々なダイアログを表示してモデルのプロパティを変更できる機構を考えます。

開発アプリPR

3分作曲-musicLine-



はじめに

モデルのプロパティを変更する際は、様々なダイアログ(ポップアップView)を表示します。

例えば、MIDI楽譜を編集するアプリmusicLineでは

  • 曲名やテンポの変更
  • 楽器の変更
  • 編集トラックの変更
  • フレーズの追加

等があります。


しかし、ダイアログのデザインは決まっており、様々なダイアログを作るたびに同じようなコードを書かなくてはいけません。
この記事では、このようなボイラープレートを無くして様々なダイアログを作成する方法を検討します。


なお、簡易なダイアログで良い場合やダイアログのデザインや挙動を使い回す必要がないのであれば、適当なViewを作成してモディファイア.confirmationDialog.sheetで表示すれば良いでしょう。



ダイアログの仕様

今回の想定するダイアログの仕様です。

  1. ヘッダーに X ボタンとタイトルを表示
  2. X ボタンかダイアログ外をタップして閉じる
  3. ダイアログの中身を変更できるようにする

またダイアログは、こんな感じでソースのどこからでも呼び出せるようにしたいと思います。

myDialog.open(model: instance)


実装

今回は各々のボタンをタップすることで、3種類のダイアログが表示されるViewを作ってみました。


共通ダイアログ

まず、仕様を満たす抽象的なダイアログとそのモデルを定義します。

struct Dialog<TView: View, TDialogModel: DialogModelable>: View {
    
    let dialogModel: TDialogModel
    @ViewBuilder let content: () -> TView
    ...

    var body: some View {
        if dialogModel.isOpen {
            ZStack{
                // 黒背景(タップで閉じる)
                blackScreen
                
                // ダイアログの中身
                content()
                    ...
            }
            .zIndex(1)
        }
    }
    
    private var blackScreen: some View {
        Color.black
            .opacity(0.3)
            .ignoresSafeArea()
            .onTapGesture {
                dialogModel.close()
            }
            .contentShape(Rectangle())
    }
    ...

}
protocol DialogModelable{
    var title: String { get }
    var isOpen: Bool { get }
    func close()
}

dialogModel.isOpenにより、表示と非表示を切り替えます。
背景blackScreenをタップした時に、dialogModel.close()でダイアログを閉じます。
ダイアログの中身は派生ダイアログでカスタマイズできるように、contentクロージャーにしています。


また、Dialogのヘッダーの実装について、

struct Dialog<TView: View, TDialogModel: DialogModelable>: View {
    
    ...
    private let headerHeight = 35.0
    
    var body: some View {
    ...
                // ダイアログの中身
                content()
                    .padding(.top, headerHeight)
                    .overlay(titleBar)
                    ...
    }
    
    // ヘッダー
    private var titleBar: some View {
        VStack{
            ZStack(alignment: .top){
                Color.gray.frame(height: headerHeight)
                HStack{
                    Image(systemName: "multiply")
                        .onTapGesture {
                            dialogModel.close()
                        }
                        .padding(10)
                    Spacer()
                }
                Text(dialogModel.title)
                    .lineLimit(1)
                    .font(.headline)
                    .padding(6)
            }
            Spacer()
        }
    }
}

content().paddingで上に空間を作って、overlayでヘッダーを付けてます。
ヘッダーでは X ボタンとタイトルdialogModel.titleを表示しています。


ダイアログをカスタマイズ

次に、DialogDialogModelableを派生させて、独自のダイアログへカスタマイズします。
例えば、ダイアログの中身をステッパーにする場合

struct NumberStepperDialog: View {
    
    @Bindable var numberStepper: NumberStepperDialogModel
    
    var body: some View {
        Dialog(dialogModel: numberStepper){
            Stepper("\(numberStepper.number.value)",value: $numberStepper.number.value)
                .frame(width: 200)
        }
    }
}
@Observable 
class NumberStepperDialogModel: DialogModelable{
    let title = "Stepper"
    var number = Number()
    private(set) var isOpen = false
    
    func open(number: Number){
        self.number = number
        isOpen = true
    }
    
    func close(){
        isOpen = false
    }
}

なお、Viewへの通知はObservationを使っているので、iOS17以降で動作します。
それ以前でしたら、Combine等に置き換えてください。


ダイアログモデルをシングルトンで管理

ダイアログをどこからでも呼び出せるようにシングルトンでダイアログモデルを管理します。

class DialogManager {
    
    static let shared = DialogManager()
    let numberStepper = NumberStepperDialogModel()
    ...
}
@Observable 
class Number{
    var value = 0
}
let number = Number()    // 編集対象モデル

// ダイアログを呼び出す
DialogManager.shared.numberStepper.open(number: number)

ダイアログを貼り付ける

呼び出された時にダイアログを表示できるように、Viewにダイアログを貼り付けます。

struct ContentView: View {

    ...
    let dm = DialogManager.shared

    var body: some View {
        ZStack{

            NumberStepperDialog(numberStepper: dm.numberStepper)
            ...
        }
    }

}

ソースコード等の詳細は元記事へ



おわりに

今回はSwiftUIでダイアログを表示する方法を検討しました。
ダイアログはデザインや挙動が似てくるので共通処理をまとめて、ボイラープレートを減らしました。
実際にはanimationをつけたり、キーボード表示の挙動など考慮する点があると思います。
あとシングルトンを使ってますが、他の方法でDIした方がいいかもしれないです。
まだまだ検討の余地はありそうですね。

開発アプリPR

3分作曲-musicLine-


1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?