読み書きプログラミング

日常のプログラミングで気づいたことを綴っています

SwiftUIのpopover

SwiftUIのpopover、なかなか手強いですね。

iPadで「できた!」と思ってもiPhoneでポップオーバーじゃなくてシートになったり。
これはiOS 16.4+ではpopoverのコンテントにpresentationCompactAdaptation(.none)修飾子をつけることで解決します。

さて、長いテキストをポップオーバーさせようとすると行数が3行程度しか表示されません。

struct ContentView: View {
    @State private var isPopoverOpen = false
    var body: some View {
        Spacer()
        Text("Hello, World!")
            .popover(isPresented: $isPopoverOpen) {
                Text("""
                        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
                        """
                )
                .fixedSize(horizontal: false, vertical: true)
                .padding()
                .presentationCompactAdaptation(.none)
                
            }
    }
}
popoverに長いテキスト

iOS16から利用可能になったLayoutプロトコルによると、SwiftUIではsizeThatFitsメソッドを通じてサイズに関して親ビューと交渉して、交渉後のサイズで子ビューをレイアウトするという仕組みらしいです。

popoverは何かの理由でこの交渉に失敗してそうです。

ではちゃんと交渉するLayoutを作りましょう。

struct PopoverContainer: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        guard subviews.count == 1 else {
            fatalError("You need to implement your layout!") // 用途がレイアウトではなく交渉なので子ビューは1つに限定しています
        }
        var p = proposal
        // 提案がnil(自由にしてよい)の場合もスクリーンサイズの制約があるのでそれを採用する。
        p.width = p.width ?? UIScreen.main.bounds.width
        p.height = p.height ?? UIScreen.main.bounds.height
        return subviews[0].sizeThatFits(p) // negotiates possible size
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        // 何もしないと子ビューをoriginに単に置く動作のようです
    }
}

このコンテナでpopoverのコンテントをラップします。

struct ContentView: View {
    @State private var isPopoverOpen = false
    var body: some View {
        Spacer()
        Text("Hello, World!")
            .popover(isPresented: $isPopoverOpen) {
                PopoverContainer {
                    Text("""
                            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
                            """
                    )
                    .fixedSize(horizontal: false, vertical: true)
                    .padding()
                }
                .presentationCompactAdaptation(.none)
            }
    }
}
第1版

うーん、微妙にはみ出ました。
どうやら、スクリーンサイズからの希望では、popover側のパディング/マージンが足らず最終交渉で値切られているようです。
交渉時にpopoverに必要そうな余裕を残してみましょう。
(popover側にちゃんと最初から交渉しろよと言いたいですが…)

struct PopoverContainer: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let popoverPadding: CGFloat = 40 // これがpopover側にあげる余裕
        guard subviews.count == 1 else {
            fatalError("You need to implement your layout!")
        }
        print(#function, proposal)
        var p = proposal
        p.width = p.width ?? (UIScreen.main.bounds.width - popoverPadding)
        p.height = p.height ?? (UIScreen.main.bounds.height - popoverPadding)
        print(p, subviews[0].sizeThatFits(p))
        return subviews[0].sizeThatFits(p) // negotiates possible size
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        print(#function, proposal)
        // entrusts default
    }
}

結果。

第2版

popoverPaddingがハードコードなのは気に入りませんが、とりあえずうまく行きました。

Enjoy programming!