しめ鯖日記

しめ鯖日記

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

SwiftUIのbuildifメソッドを確認する

SwiftUIでif文が使えるのはbuildIfに自動変換されていると聞いたのでbuildIfを見てみました

buildIfメソッドはViewBuilderのメソッドで下のようなものになります

extension ViewBuilder {
    public static func buildIf<Content>(_ content: Content?) -> Content? where Content : View
}

引数を見る限り、if文は下のように3項演算子に変換されているかと思われます
if文の中は複数のViewを配置できるので、buildBlockにしています

ViewBuilder.buildIf(1 == 1 ? ViewBuilder.buildBlock(Text("Hello1"), Text("Hello2")) : nil)

elseがある場合、buildEitherというメソッドが代わりに使われるようです

参考URL

【SwiftUI】宣言的アプローチを実現するViewBuilderの仕組み #iOS - Qiita

SwiftUIのmaskで画像を切り抜いてみる

SwiftUIのマスクを使って色々試してみました

画像素材は下サイトのものを使わせてもらいました

フリーテクスチャ素材館/Calkboard(黒板)テクスチャのシームレスパターン4種類(PHOTO)

まずは円形に切り抜いてみました

struct ContentView: View {
    var body: some View {
        Image("texture")
            .frame(width: 200, height: 200)
            .mask(Circle().frame(width: 100, height: 100))
    }
}

起動すると下のような表示になります

マスクは複数の要素を並べる事もできます

struct ContentView: View {
    var body: some View {
        Image("texture")
            .frame(width: 200, height: 200)
            .mask(
                HStack {
                    Circle().frame(width: 100, height: 100)
                    Rectangle().frame(width: 100, height: 100)
                }
            )
    }
}

LinearGradientを使う事で徐々に透過度が上がるようにもできます

struct ContentView: View {
    var body: some View {
        Image("texture")
            .frame(width: 200, height: 200)
            .mask(
                Circle().fill(
                    LinearGradient(
                        gradient: .init(colors: [.black, .clear]),
                        startPoint: .top,
                        endPoint: .bottom
                    )
                ).frame(width: 100, height: 100)
            )
    }
}

Textでマスクする事もできます

struct ContentView: View {
    var body: some View {
        Image("texture")
            .frame(width: 200, height: 200)
            .mask(Text("Hello").font(.system(size: 70, weight: .black)))
    }
}

文字部分だけを切り抜く方法は下の通りです
overlayは{}内のViewを重ねるmodifierでblendModeのdestinationOutはその部分だけ除外する処理です

struct ContentView: View {
    var body: some View {
        Image("texture")
            .frame(width: 200, height: 200)
            .mask {
                Rectangle().overlay(alignment: .center) {
                    Text("Hello").font(.system(size: 70, weight: .black)).blendMode(.destinationOut)
                }
            }
    }
}

XcodeでGitHub Copilotを使う

GitHub CopilotがXcodeを公式をサポートしたようなので試してみました
ただBeta Previewsなので今後変更される事が多いことは注意が必要です
それとGithub Copilotは事前に契約しておく必要があります

github.com

GitHubのページのLatest Releaseからdmgファイルをダウンロードします

https://github.com/github/CopilotForXcode/releases/latest/download/GitHubCopilotForXcode.dmg

GitHub Copilot for XcodeをApplicationsに入れます

GitHub Copilot for Xcodeを起動すると設定を求められるので、それに従って設定を進めます

Xcodeを再起動するとメニューのEditorにGitHub Copilotが追加されていました

エディターの方は何もしなくても補完は動いてくれました
Tabを押すと補完内容を反映してくれます
Optionを押すと複数行分の補完を一気に表示できます

本来はGitHubへのログインが必要らしいのですが、今回は設定せずに使う事ができました
過去行った設定が残っていたためかもしれません

精度を試すために下のコメントを書いてCopilotに補完してもらいました

struct ContentView: View {
    var body: some View {
        VStack {
            // テキストフィールドを2つとボタンを1つ表示する
            // ボタンを押すとテキストフィールドの値をCSV形式で出力する
        }
        .padding()
    }
}

結果は下の通りです
CSVエクスポートする処理は書いてくれなかったのですが、テキストフィールドやボタンはしっかり配置してくれました

struct ContentView: View {
    var body: some View {
        VStack {
            // テキストフィールドを2つとボタンを1つ表示する
            // ボタンを押すとテキストフィールドの値をCSV形式で出力する
            TextField("名前", text: .constant("名前を入力してください"))
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            TextField("年齢", text: .constant("年齢を入力してください"))
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            Button(action: {
                // ボタンを押したときの処理
                print("ボタンが押されました")
            }) {
                Text("CSV形式で出力")
            }
        }
        .padding()
    }
}

参考URL

GitHub Copilot を Xcode で使う(GitHub 公式の Xcode 機能拡張)

SwiftUIのStyleを使ってみる

SwiftUIのStyleを使うとデザインの共通化ができるようなので調べてみました

Styleの定義方法は下の通りです
ButtonStyleに準拠した構造体を作ってmakeBodyメソッドを定義します

struct CustomButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding()
            .font(.system(size: 17, weight: .bold))
            .foregroundColor(.white)
            .background {
                RoundedRectangle(
                    cornerSize: .init(width: 8, height: 8),
                    style: .continuous
                )
                .fill(.tint)
            }.opacity(configuration.isPressed ? 0.7 : 1.0)
    }
}

次にButtonStyleにメソッドを追加します

extension ButtonStyle where Self == CustomButtonStyle {
    static var custom: CustomButtonStyle {
        CustomButtonStyle()
    }
}

Styleの使い方は下の通りです

struct ContentView: View {
    var body: some View {
        Button("test1!") {
            print("tap!")
        }.buttonStyle(.custom).tint(Color.red)
        
        Button("test2!") {
            print("tap!")
        }.buttonStyle(.custom).tint(Color.green)
    }
}

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

Styleは下のように引数を渡す事もできます

struct CustomButonStyle: ButtonStyle {
    let foregroundColor: Color
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .foregroundStyle(foregroundColor)
    }
}

extension ButtonStyle where Self == CustomButonStyle {
    static func custom(foregroundColor: Color) -> CustomButonStyle {
        CustomButonStyle(foregroundColor: foregroundColor)
    }
}

struct ContentView: View {
    var body: some View {
        Button("test") {
            
        }.buttonStyle(.custom(foregroundColor: .red))
    }
}

Styleは他にもLabelなどいくつかのViewで使う事ができます

struct CustomLabelStyle: LabelStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.title
            .font(.system(size: 17, weight: .bold))
            .foregroundColor(.red)
        configuration.icon.foregroundStyle(.green)
    }
}

extension LabelStyle where Self == CustomLabelStyle {
    static var custom: CustomLabelStyle {
        CustomLabelStyle()
    }
}

使えるViewについては下URLが参考になりました

View styles | Apple Developer Documentation

参考URL

SwiftUIのStyleの作り方 - 共通のUIデザインはStyleに切り出す · すっさんぽ

SwiftのTextKit2の動作を確認する

下のスライドが面白かったので実際に自分でも動作を確認してみました

TextKit 2 時代の iOS のキーボードとテキスト入力と表示のすべて - Speaker Deck

まずはプロジェクトを作って普通にUITextViewを表示してみました

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let textView = UITextView()
        textView.text = "test!!"
        textView.sizeToFit()
        textView.center = view.center
        view.addSubview(textView)
    }
}

確認した所、layoutManagerとtextLayoutManagerの両方がある事を確認できました

print(textView.layoutManager)
print(textView.textLayoutManager)

layoutManagerを使うとグリフの文字サイズの位置を取得できる事を確認できました
また、取得した位置はUITextViewのxyからの相対距離になります
それとtextLayoutManagerは下のようなglyphRangeなどのメソッドはありませんでした

let layoutManager = textView.layoutManager
let range = layoutManager.glyphRange(forCharacterRange: NSRange(location: 5, length: 1), actualCharacterRange: nil)
print(layoutManager.boundingRect(forGlyphRange: range, in: textView.textContainer))

次はtextLayoutManagerを使って各行のサイズや文字列や文字の位置など様々なものを取得しました

textView.text = "test\ntest!!!"

let textLayoutManager = textView.textLayoutManager!
let textContentManager = textLayoutManager.textContentManager
textLayoutManager.enumerateTextLayoutFragments(from: textContentManager?.documentRange.location) { layoutFragment in
    layoutFragment.textLineFragments.forEach { textLineFragment in
        print(textLineFragment.typographicBounds)
        print(textLineFragment.attributedString)
        print(textLineFragment.locationForCharacter(at: 3))
    }
    return true
}

検証中に気がついたのですが、layoutManagerに触るとtextLayoutManagerはnilになりました
スライド中でTextKit1のAPIに触れるとTextKit1固定になるとあったのに関連しているのかと思います

print(textView.textLayoutManager) // → Optional(<NSTextLayoutManager>)
print(textView.layoutManager)
print(textView.textLayoutManager) // → nil

NSLayoutManagerですがUITextViewなしで動かす事も可能です
文字の位置だけ知りたい時などに便利そうです

let layoutManager = NSLayoutManager()
let textStorage = NSTextStorage(attributedString: NSAttributedString(string: "test\niest"))
let textContainer = NSTextContainer(size: CGSize())
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)

let range = layoutManager.glyphRange(forCharacterRange: NSRange(location: 5, length: 2), actualCharacterRange: nil)
print(layoutManager.boundingRect(forGlyphRange: range, in: textContainer))

フォントを変えたい場合はNSAttributedStringにセットすれば良さそうです

let text = NSAttributedString(string: "test\niest", attributes: [.font : UIFont.systemFont(ofSize: 24.0)])

軽く触ってみたのですが知らないAPIが数多くあって面白かったです
今後より細かい文字列の操作をする時に使ってみたいと思います

xcstrings-toolでxcstringsの翻訳データの補完ができるようにする

xcstrings-toolというツールを使うとxcstringsの翻訳データを補完できるようになるらしいので調べてみました

github.com

今回はSwiftPackageManagerを使いました
CocoaPodsやCarthageは未対応のようです

インストールしたらプラグインを追加します
プラグインはBuild PhasesのRun Build Tool Plug-insのプラスボタンから追加できます

次にxcstringsファイルを追加します

ひとまず英語と日本語を追加してtest1という翻訳データを追加しました

上記を行うと下のようにすれば翻訳データを取れるようになります

String(localizable: .test1)

文字に数字が含まれている場合は下のように引数で渡す形になります

String(localizable: .test1(1000))

簡単に試してみたのですがシンプルで使いやすそうなので今後活用してみたいと思います

Layout protocolで柔軟なレイアウトを実現する

iOS 16で登場したLayout protocolというのを使うと柔軟なレイアウトができるようなので調べてみました

使い方ですが、まずは下のようにCustomLayoutを定義します
CustomLayoutはVStackやHStackのように親ビューとして使う事ができます

struct CustomLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        return CGSize(width: 100, height: 100)
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        subviews.enumerated().forEach {
            $0.element.place(
                at: CGPoint(x: Int(bounds.origin.x) + $0.offset * 10,
                            y: Int(bounds.origin.y) + $0.offset * 10),
                anchor: .topLeading,
                proposal: .unspecified)
        }
    }
}

実際に使っているコードは下のものです
CustomLayoutの下にTextを2つ並べています

struct ContentView: View {
    var body: some View {
        CustomLayout {
            Text("1")
            Text("2")
        }.background(.red)
    }
}

起動すると下のような表示になります

続いてLayout protocolで使っている2つのメソッドについて見ていきます
sizeThatFitsですがこれはCustomLayoutのサイズを決めるものです
今回は100x100のサイズ固定にしました

func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
    return CGSize(width: 100, height: 100)
}

通常はsubviewのサイズを元に調整する事が多いかと思います
小ビューの大きさはsizeThatFitsで取れるのでそれを利用します

subviews.forEach { print($0.sizeThatFits(.unspecified)) }

もう一つのplaceSubviewsはサブビューの位置を決めるものです
placeメソッドで位置を決めています

今回は1つ目の要素が0x0の位置、2つ目は10x10の位置というように斜めに配置しました
位置ですがbounds.origin.xとyを足さないと画面の左上に配置されてしまうので注意が必要です

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
    subviews.enumerated().forEach {
        $0.element.place(
            at: CGPoint(x: Int(bounds.origin.x) + $0.offset * 10,
                        y: Int(bounds.origin.y) + $0.offset * 10),
            anchor: .topLeading,
            proposal: .unspecified)
    }
}

CustomLayoutですがframeで大きさを決める事も可能です
ただframeはplaceSubviewsが走った後に反映されるので使わない方が良さそうです

CustomLayout {
    Text("1")
    Text("22")
}.frame(width: 200, height: 200).background(.red)

参考URL

SwiftUIのLayout protocolについて #iOS - Qiita