cockscomblog?

cockscomb on hatena blog

紛らわしい文字列をもっと紛らわしくする

同僚にid:yashigani_wヤシガニ)とid:yigarashi(ワイイガラシ)がいる。いかにも似ているが、あるとき一緒に仕事をすることになって、紛らわしさが限界を超えた。

yashigani
yigarashi

そうとわかって見たら、なるほど「y」で始まって「i」で終わるのが似ているね、くらいに思うかもしれない。しかし不意にSlackやGitHubで見かけると、一瞬どちらかわからない。

この紛らわしさをより俯瞰してみるため、画像にしてみました。

f:id:cockscomb:20200101161439j:plain
Photoshopで作った

どうだ紛らわしいだろう。全体的な形がなんだか似ている。

ところでこの画像はPhotoshopで作った。テキストにガウシアンぼかしをかけて、その後から二値化した。なかなかかっこいいから、もっといろいろ試してみたい。

前置きが長くなったが、テキストにガウシアンぼかしをかけて、その後から二値化 するのを、macOSiOSの仕組みでやってみるのが、本題である。

Core Textでテキストの画像を作る

まずはテキストを画像化しないと始まらない。テキストの画像を作るのは、Core Text(とCore Graphics)で素朴に実装できる。

import CoreGraphics
import CoreText
import Foundation

struct Padding {
    let top: CGFloat
    let left: CGFloat
    let bottom: CGFloat
    let right: CGFloat

    static var zero: Padding {
        return Padding(top: 0, left: 0, bottom: 0, right: 0)
    }
}

func render(text: NSAttributedString, padding: Padding = .zero) -> CGImage {
    let framesetter = CTFramesetterCreateWithAttributedString(text)
    let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(
        framesetter,
        CFRange(),
        nil,
        CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
        nil)

    let width = Int(ceil(frameSize.width + padding.left + padding.right))
    let height = Int(ceil(frameSize.height + padding.top + padding.bottom))

    let ctx = CGContext(
        data: nil,
        width: width,
        height: height,
        bitsPerComponent: 8,
        bytesPerRow: width * 4,
        space: CGColorSpaceCreateDeviceRGB(),
        bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
    guard let context = ctx else {
        fatalError()
    }

    context.saveGState()
    context.setFillColor(.white)
    context.fill(CGRect(origin: CGPoint.zero, size: CGSize(width: width, height: height)))
    context.restoreGState()

    let path = CGPath(
        rect: CGRect(
            origin: CGPoint(
                x: (CGFloat(width) - frameSize.width) / 2,
                y: (CGFloat(height) - frameSize.height) / 2),
            size: frameSize),
        transform: nil)
    let frame = CTFramesetterCreateFrame(framesetter, CFRange(), path, nil)
    CTFrameDraw(frame, context)

    guard let image = context.makeImage() else {
        fatalError()
    }
    return image
}

NSAttributedStringからCore TextのAPIを使ってCGImageを作る。このときpaddingを設定できるようにすると、あとで都合がいい。

Core Imageで画像を加工する

画像の加工ではいつものように、Core Imageを使うことを検討する。以前CoreImage.CIFilterBuiltinsで試したように、CIFilter.gaussianBlur()を使えば、ガウシアンぼかしは簡単だ。あとは二値化するだけだが、ここで手が止まる。意外なことに、組み込みのフィルターには二値化が存在しない。

Metal Shading Languageによるカスタムカーネル

二値化は簡単そうなので手作りする。Core ImageカーネルをMetal Shading Languageで書く。ThresholdBinary.metalを以下の内容で作った。またXcodeのBuild Settingsで、Other Metal Compiler Flags-fcikernelを、User-Defined Settingsに追加したMTLLINKER_FLAGSキーに-cikernelを、それぞれ設定する必要がある。

#include <metal_stdlib>
#include <CoreImage/CoreImage.h>
using namespace metal;

extern "C" {
    namespace coreimage {
        float4 threshold_binary(sample_t source, float threshold) {
            float4 image = premultiply(source);
            float y = 0.299 * image.r + 0.587 * image.g + 0.114 * image.b; // BT.601
            float binary = y < threshold ? 0.0 : 1.0;
            return float4(binary, binary, binary, 1.0);
        }
    }
}

RGB値から輝度成分を取り出すために、YUV色空間のYを計算する。これが閾値を超えていれば1、そうでなければ0とすることで、二値化できる。

このカーネルは以下のように読み込める。

import CoreImage

let thresholdBinaryKernel: CIColorKernel = {
    guard
        let url = Bundle.main.url(forResource: "default", withExtension: "metallib"),
        let data = try? Data(contentsOf: url)
    else {
        fatalError("Unable to get metallib")
    }
    guard
        let kernel = try? CIColorKernel(
            functionName: "threshold_binary", fromMetalLibraryData: data)
    else {
        fatalError("Unable to create CIKernel from threshold_binary")
    }
    return kernel
}()

Core Imageによる画像の加工

二値化のカーネルが書けたので、あとは実際に画像を加工するだけだ。

import CoreImage
import CoreImage.CIFilterBuiltins

let ciContext: CIContext = CIContext()
func process(image: CGImage, sigma: Float, threshold: Float) -> CGImage? {
    let ciImage = CIImage(cgImage: image)

    let gaussianBlur = CIFilter.gaussianBlur()
    gaussianBlur.inputImage = ciImage.clampedToExtent()
    gaussianBlur.radius = sigma
    guard let blurredImage = gaussianBlur.outputImage else {
        return nil
    }

    guard
        let processedImage = thresholdBinaryKernel.apply(
            extent: ciImage.extent, arguments: [blurredImage, threshold]
        )
    else {
        return nil
    }

    guard
        let cgImage = ciContext.createCGImage(
            processedImage, from: processedImage.extent)
    else {
        fatalError()
    }
    return cgImage
}

CIImage.clampedToExtent()を使うのがコツで、ガウシアンぼかしをかけるときに画像の端が変な色になってしまうのを防げる。

どうですか

ここまでで、テキストにガウシアンぼかしをかけて、その後から二値化 ができた。iOSでもmacOSでも同じように動くはずだ。

f:id:cockscomb:20200106103459p:plain

なんだかちょっとかっこよくないですか。

参考文献

Metalは初めて使うので、ちょっとだけ勉強しました。

apple/swift-formatを試す

オフィシャル感のあるSwiftのformatter/linterであるところの、swift-formatを試した。

SwiftのGoogleフォークで生まれたものがベースになっているようだ。開発が進んでいるmasterブランチと、Swift 5.1に対応するswift-5.1-branchブランチがあり、後者を試している。

インストール

HomebrewでインストールしたかったのでFormulaを用意した。

$ brew install https://gist.githubusercontent.com/cockscomb/183acd19d2f5e127045dc43c6c472535/raw/63c935672b4e8c9d6f2056785283a6d6b7d31b77/swift-format.rb

ビルドに2分くらいかかった。

(もしかするとMintというのを使うといいのかもしれないが、試していない。)

ヘルプを見てみる。

$ swift-format --help
OVERVIEW: Format or lint Swift source code.

When no files are specified, it expects the source from standard input.

USAGE: swift-format [options] [filename or path ...]

OPTIONS:
  --assume-filename       When using standard input, the filename of the source to include in diagnostics.
  --configuration         The path to a JSON file containing the configuration of the linter/formatter.
  --in-place, -i          Overwrite the current file when formatting ('format' mode only).
  --mode, -m              The mode to run swift-format in. Either 'format', 'lint', or 'dump-configuration'.
  --recursive, -r         Recursively run on '.swift' files in any provided directories.
  --version, -v           Prints the version and exists
  --help                  Display available options

POSITIONAL ARGUMENTS:
  filenames or paths      One or more input filenames

dump-configuration

swift-formatの設定は、JSONで表現される。デフォルトの設定は以下のようにdumpできる。

$ swift-format --mode dump-configuration > .swift-format
{
  "blankLineBetweenMembers" : {
    "ignoreSingleLineProperties" : true
  },
  "indentation" : {
    "spaces" : 2
  },
  "indentConditionalCompilationBlocks" : true,
  "lineBreakBeforeControlFlowKeywords" : false,
  "lineBreakBeforeEachArgument" : false,
  "lineLength" : 100,
  "maximumBlankLines" : 1,
  "respectsExistingLineBreaks" : true,
  "rules" : {
    "AllPublicDeclarationsHaveDocumentation" : true,
    "AlwaysUseLowerCamelCase" : true,
    "AmbiguousTrailingClosureOverload" : true,
    "BeginDocumentationCommentWithOneLineSummary" : true,
    "BlankLineBetweenMembers" : true,
    "CaseIndentLevelEqualsSwitch" : true,
    "DoNotUseSemicolons" : true,
    "DontRepeatTypeInStaticProperties" : true,
    "FullyIndirectEnum" : true,
    "GroupNumericLiterals" : true,
    "IdentifiersMustBeASCII" : true,
    "MultiLineTrailingCommas" : true,
    "NeverForceUnwrap" : true,
    "NeverUseForceTry" : true,
    "NeverUseImplicitlyUnwrappedOptionals" : true,
    "NoAccessLevelOnExtensionDeclaration" : true,
    "NoBlockComments" : true,
    "NoCasesWithOnlyFallthrough" : true,
    "NoEmptyTrailingClosureParentheses" : true,
    "NoLabelsInCasePatterns" : true,
    "NoLeadingUnderscores" : true,
    "NoParensAroundConditions" : true,
    "NoVoidReturnOnFunctionSignature" : true,
    "OneCasePerLine" : true,
    "OneVariableDeclarationPerLine" : true,
    "OnlyOneTrailingClosureArgument" : true,
    "OrderedImports" : true,
    "ReturnVoidInsteadOfEmptyTuple" : true,
    "UseEnumForNamespacing" : true,
    "UseLetInEveryBoundCaseVariable" : true,
    "UseShorthandTypeNames" : true,
    "UseSingleLinePropertyGetter" : true,
    "UseSynthesizedInitializer" : true,
    "UseTripleSlashForDocumentationComments" : true,
    "ValidateDocumentationComments" : true
  },
  "tabWidth" : 8,
  "version" : 1
}

Documentation/Configuration.mdというドキュメントがある。

デフォルトでインデントがスペース2つだった。開発の最初期からスペース2つだったようで、要するにGoogleのSwift Style Guideに倣っているためである。SwiftUIやFunction buildersのようなDSL的なユースケースが増えてきたときに、インデントが深くなりやすいから、スペース2つの方がいいというトレンドになるかもしれない。しかしひとまず、一般的なスペース4つに変えた。

lint

$ swift-format --mode lint --configuration .swift-format --recursive .

けっこういろいろ出てくる。

--configuration .swift-formatは、ファイル名が.swift-formatの場合には省略できる。

SwiftLintを真似て、XcodeBuild PhasesRun Script Phaseを追加。

if which swift-format >/dev/null; then
  swift-format --mode lint --recursive . || true
else
  echo "warning: swift-format not installed"
fi

(lintが通らないと終了コードが1になり、後続のphaseに進まないので、|| trueしておくといい。)

こうすると、出力がXcodeの求める形式と合っているので、エディタにwarningが表示される。

OnlyOneTrailingClosureArgumentという、引数にクロージャが2つ以上あるときtrailing closureを許さないルールに引っかかる。可読性が落ちるので妥当なルールとも思うが、SwiftUIのTutorialをみると、Buttonで使っているパターンである。actionクロージャをメソッドとして切り出すのが正攻法だろうが、無効にしてもいいだろう。

NeverUseImplicitlyUnwrappedOptionalsというのでも引っかかる。var str: String!のようなのは、あまり行儀がいいとはいえないものの、Xcodeのテンプレートでも使われるパターンである。部分的にswift-formatのルールを無効にできるといいのだが。

部分的なlintの無効化

masterにはDocumentation/IgnoringSource.mdというのがあった。以下のようなコメントを書くことで、コメントが書かれた次の行からASTで1ノード分、swift-formatが無視してくれるというものらしい。

// swift-format-ignore
// swift-format-ignore: [comma delimited list of rule names]

使いたいけど、swift-5.1-branchには入っておらず、Swift 5.1ではまだ使えなさそうだった。

format

swift-formatなので、もちろんformatできる。

$ swift-format --mode format --recursive --in-place .

Makefileも用意しておく。

.PHONY: format lint
format:
    swift-format --mode format --recursive --in-place .
lint:
    swift-format --mode lint --recursive .

おもしろいところでは、ネームスペース代わりのinitを潰したstructenumに書き換えてくれた。


フォーマットの感じは悪くなく、(プロジェクトが小さいためか)パフォーマンスも特に気にならない。手元でちょっと使うのには適しているだろう。

CIで動かそうとするともうちょっと準備が必要で、競合するSwiftLintの方がノウハウが蓄積されていて便利かもしれない。

そのうち安定版がリリースされて、Swiftのツールチェーンにバンドルされたり、Xcodeとのインテグレーションが整備されたりすると、さらに使い勝手がよくなりそう。

追記

2019/12/19 18:50

--configuration [json file]が、ファイル名が.swift-formatの場合に省略できる旨を追加し、全体的に省略するようにした。

参考

息子にカメラを与える

まだ2歳半で早すぎると思うけど、普段から僕のカメラを羨ましがっているので、少しくらい乱雑に扱われても大丈夫そうなカメラを買った。

いろいろ調べて、NikonCOOLPIX W150というのにした。耐衝撃性能を宣伝しているくらいなので、きっと頑丈なつくりになっている。ブルーが子供らしくてかわいいかと思ったが、息子は「白がいい」というので、ホワイトにした。子供は子供らしいものを欲しがらない。

Nikonの「(子供も含めた)家族みんなで楽しめる」というコンセプトは、2012年のCOOLPIX S30の頃に始まったようで、2013年のCOOLPIX S31、2014年のセンサーが変更されたCOOLPIX S32、2015年のCOOLPIX S33とシリーズが続き、2016年にはWi-FiBluetoothに対応したCOOLPIX W100が発売されている。そして毎年だったモデルチェンジのサイクルが変わったのか、2019年になって発売になったのが、COOLPIX W150ということのようだ。

画質は何世代か前のiPhoneくらいだが、耐衝撃・耐水・防塵で、子供にも扱いやすいインターフェースを備えている。光学3倍ズームとかフラッシュとかも付いているけど、今のところそんなに活用していない。特にフラッシュは、誤って子供が有効にしないか気がかりである。上面にストラップ取り付け部が2つ付いているのはポイントが高い。ネックストラップは付属しないので別に買った。子供はカメラを落としたり、その辺に置いてきたりしがちなので、ネックストラップがあるとよいが、長さの調節幅が大きいものを選ばないといけない。さらに別売のソフトケースも買った。

f:id:cockscomb:20191026095816j:plain
カメラを構える息子

ということで妻が入院している最中にカメラを買って、一週間と少し経った。息子はどこへ行くにもカメラを首から下げて、たまに思い出したら写真を撮る。大人の真似をして妹である赤ちゃんの写真を撮っているときは感慨深かった。とはいえちゃんとした写真が撮れるのはまだ稀だ。しかし息子自身はそんなことお構いなしで、自分用のカメラを喜び、写真を撮る行為そのものを楽しんでいる。まだおもちゃみたいなものだけど、そのうち真価に気付くことを期待している。

小さい子供にカメラを持たせると困ることもある。カメラに夢中になって注意散漫になるので、道を歩くときに危ない。ストラップを持ってカメラを振り回すことがある。撮っていいとか悪いとかの区別がない。家の中とか明らかに安全な場所以外では、親がちゃんと見ていられる時にだけカメラを持たせるようにしないといけない。

なんにせよこれで僕のGR IIIへの関心が下がって、寿命が伸びた。GR IIIもおすすめです。

CoreImage.CIFilterBuiltins

iOSmacOSで画像を加工しようとした場合、Core Image frameworkを使うのが簡単だ。画像をハードウェアの支援で高速に処理でき、内蔵された様々なフィルターを使うこともできる。Mac OS X 10.4で初めて搭載されて以来、しっかりとアップデートされ続けている。

2019年のiOS 13やmacOS 10.15では、Core Imageの色々なフィルターをSwiftから扱いやすくなった。

CIFilter APIの変遷

最初期は以下のように、Key-Value Codingを活用したAPIだった。もともとCore Image frameworkは、GUIでユーザーに操作させることを想定しており、文字列などから動的に扱えるように作られている。(あるいは組み込みではないフィルターがインストールされている場合もあり、いずれにしてもKVCは有用である。)反面で、フィルター名やフィルターとパラメータの対応に気を配る必要がある。

import CoreImage

let image: CIImage = ...

guard let filter = CIFilter(name: "CIGaussianBlur") else {
    fatalError()
}
filter.setValue(20, forKey: kCIInputRadiusKey)
filter.setValue(image, forKey: kCIInputImageKey)
guard let processed = filter.value(forKey: kCIOutputImageKey) as? CIImage else {
    fatalError()
}

let context = CIContext()
context.createCGImage(processed, from: processed.extent)

iOS 8のタイミングで、KVCではないAPIも整備され、CIFilter.init(name:parameters:)CIImage.outputImageを使って、以下のように書けるようになった。

guard let filter = CIFilter(name: "CIGaussianBlur", parameters: [
    kCIInputRadiusKey: 20,
    kCIInputImageKey: image,
]) else {
    fatalError()
}
guard let processed = filter.outputImage else {
    fatalError()
}

さらにこれのショートカットで、CIImage.applyingFilter(_:parameters:)メソッドを使い、以下のようにも書ける。見た目はかなりよくなったが、ガウシアンぼかしのために"CIGaussianBlur"フィルターを使って、パラメータとしてkCIInputRadiusKeyを与える、ということは知らなければならない。

let processed = image.applyingFilter("CIGaussianBlur", parameters: [
    kCIInputRadiusKey: 20,
])

iOS 10の頃にはCIImage.applyingGaussianBlur(sigma:)メソッドなどが追加され、いくつかの基本的なフィルターが容易に利用可能になった。

let processed = image.applyingGaussianBlur(sigma: 20)

CIFilterBuiltins

iOS 13やmacOS 10.15から、新たにCIFilterにたくさんのクラスメソッドが追加された。このうちCIFilter.gaussianBlur()を使えば、これまでと同じ処理を以下のように書ける。

import CoreImage
import CoreImage.CIFilterBuiltins

let image: CIImage = ...

let filter = CIFilter.gaussianBlur()
filter.radius = 20
filter.inputImage = image
guard let processed = filter.outputImage else {
    fatalError()
}

let context = CIContext()
context.createCGImage(processed, from: processed.extent)

クラスメソッドのシグネチャclass func gaussianBlur() -> CIFilter & CIGaussianBlurになっているので、型を利用して簡単に扱えるようになっている。このようなクラスメソッドが、組み込みのフィルターそれぞれに用意されている。

import CoreImage.CIFIlterBuiltinsを忘れやすいので注意が必要だ。Core Imageのmodulemapをみると、以下のようになっている。

framework module CoreImage [extern_c] {
  umbrella header "CoreImage.h"
  export *
  module * { export * }
  
  explicit module CIFilterBuiltins {
      header "CIFilterBuiltins.h"
      export *
  }
}

まとめ

Core ImageAPIは徐々に改良が加えられている。

ユースケースに応じて異なるAPIを使うのがよいだろう。単純にガウシアンぼかしを掛けたいのであれば、CIImage.applyingGaussianBlur(sigma:)が最も簡単である。こういったショートカットが用意されていない組み込みのフィルターを利用するなら、CIFilterBuiltinsがよい。組み込みではないフィルターを利用するような場合は、必然的にCIImage.applyingFilter(_:parameters:)メソッドやKVCを使うことになる。

二人目の子供が生まれました

およそ一週間前の21日午前1時頃、僕と妻の二人目の子供が生まれました。予定日を10日も過ぎて生まれた息子とは違い、予定日翌日のことでした。3,200グラムの女の子です。日が変わる頃に病院へ行って、そこからあっという間に産まれました。

息子のときより小さいせいか、なんだか違った風に感じています。その息子もまた新たな家族の誕生を喜んでおり、赤ちゃんの顔を覗き込んだり、なんだかんだと話しかけたりしています。

二人目ということで、僕にも妻にもいくらか心の余裕がありましたが、一方で前回とは違って2歳半になった息子もいるので、心配もしました。実際にこの一週間は僕と息子の二人で生活していましたが、息子の成長に驚かされるばかりでした。この二人暮らしのことは、僕にとっても生涯の思い出になりそうです。

この誕生に伴って、僕も少しの間だけ育児休業させていただくことにしました。仕事をしていない自分への恐れもありますが、しっかり子育てにコミットして、これまでと違った自分を見つけたいとも思っています。

引き続きよろしくお願いいたします。

Chromeが影響を受けたmacOS Catalinaの「Hiragino Kaku Gothic ProN」フォントファミリーに関する変更

macOS Catalinaにアップグレードすると、Google Chromeのフォントの表示が変わる、ということがあった。CSSでフォントファミリーとして「Hiragino Kaku Gothic ProN」を指定していても、この指定が効かずにフォールバックされてしまうというものである。macOS Catalinaから、「Hiragino Kaku Gothic ProN(ヒラギノ角ゴ ProN)」ファミリーが削除され、「Hiragino Sans(ヒラギノ角ゴシック)」だけになったのが原因とされている。

同僚のid:polamjagが教えてくれたのだが、Chromiumのbug trackerにissueが登録されている。

内容を読むと、単に「Hiragino Kaku Gothic ProN」ファミリーが削除されたというだけでなく、NSFontManager APIの呼び出し方によっても結果が異なるようだ。

NSFontManager.availableFontFamiliesの結果が「Hiragino Kaku Gothic ProN」を含まなくなり、NSFontManager.availableMembers(ofFontFamily:)を呼べば「Hiragino Kaku Gothic ProN」が見つかる。

import Cocoa

let hiraginoKakuGothicProN = "Hiragino Kaku Gothic ProN"

let fontManager = NSFontManager.shared

print(fontManager.availableFontFamilies.filter { $0 == hiraginoKakuGothicProN }) // []

if let hiragino = fontManager.availableMembers(ofFontFamily: hiraginoKakuGothicProN) {
    print(hiragino) // [[HiraKakuProN-W3, W3, 4, 0], [HiraKakuProN-W6, W6, 8, 2]]
}

NSFontManager.availableMembers(ofFontFamily:)でフォントが見つかるのは、Appleが互換性を保とうとしたからだろう。

Chromeはフォントファミリーをcase insensitiveで見つけるため、敢えてNSFontManager.availableFontFamiliesの方を使っていたということだったが、修正コミットとなるTest for font family on mac using NSFontManager::availableFontsForFamilyの該当部分をみると、NSFontManager.availableMembers(ofFontFamily:)を呼び出すようになった。説明によれば、NSFontManager APIは、case insensitiveにフォントを見つけてくれるようだ。

Marzipanを予想した答え合わせ #WWDC19

昨年のWWDCよりさらに前に、Marzipanを予想すると称して、与太話を書いた。その時は「UIKitの移植」と「まったく新しいUIフレームワーク」とそれらの「グラデーション」の3パターンを予想していたが、結果としてはこれらがすべて出た。

Project Catalyst

初めに、Marzipanと呼ばれていたものは、新たに「Project Catalyst」と呼ばれることになった。しかしAppleのドキュメントの中ではこのプロジェクト名ではなく、「UIKit for Mac」と記載されている。これこそ「UIKitの移植」である。

UIKitの移植とともにUIKitを拡張して、コンテキストメニューやメニューバーなど、macOSの基本的な操作に適合できるようにしている。ホバーがUIHoverGestureRecognizerとしてUIGestureRecognizerになっていたり、コンテキストメニューUIContextMenuInteractionとしてUIInteractionになっていたり、抽象度が妙に高いような傾向がみられる。

そのほかにもmacOSに合わせてカスタマイズする方法がいくつも提供されてはいるものの、AppKitでmacOSアプリを作るのと全く遜色ない、とまではいかないように思われる。UIKit for Macは、iOS、特にiPadOSに最適化されたアプリをmacOSへ移植し、クロスプラットフォーム開発を行う目的で利用するのが、自然なユースケースである。

SwiftUI

SwiftネイティブなUIフレームワークがこれほど早くに出てきたことには驚かされた。SwiftUIは昨年の予想における「まったく新しいUIフレームワーク」がまさに具現化したものである。SwiftUIにはView Controllerすらない。

SwiftUIは、最近ではReactに代表され、そしてFlutterやJetpack Composeもまたそうであるように、宣言的なUIフレームワークだ。そしてAPIはSwiftネイティブと呼ぶにふさわしい。Objective-Cの動的な機能がなくてもうまく動くように、CombineというRxめいた新しいフレームワークが様々に機能するようだが、まだ全容を掴みきれていない。

SwiftUIはmacOSiOS、tvOSそしてwatchOSに渡って利用できる。プラットフォームに合わせたUIについても配慮されている。ただしSwiftUIはこの秋にリリースされる最新のOSから対応となるため、すぐにSwiftUIへ移行できる開発者はごく少数だろう。

適応

UIKit for Macのねらいは、MaciPadの距離を近づけることにあるだろう。Macにはアプリが少なく、一方のiPadにはプロユースに耐えうるアプリが足りない。これをまとめて解決するためのクロスプラットフォームである。多くのiOSアプリは、Xcodeプロジェクトでチェックボックスをひとつ有効にするだけで、macOSでも動作するようになる。

反対にSwiftUIは、次世代のUIフレームワークという側面が強い。AppKitも、それよりはモダンなUIKitでさえも、すでに10年以上の歴史を持っている。Objective-CがSwiftになってパラダイムが変化したのに伴い、UIフレームワークの再設計を行なったものだ。少しずつ取り入れやすいように、既存のアプリを少しずつ置き換えられるようになっている。

昨年の「グラデーション」で示したように、いずれも既存のアプリで取り入れやすいように工夫がなされている。


ひととおり振り返ってみたところ、全体的に大意は当たっていたように思う。予想が当たったというより、演繹的に考えれば誰でも同じような考えに辿り着くようなものではある。

この何年か、他のプラットフォームにおける開発環境の改善と比して、Appleが水をあけられつつある、というような印象を抱くことがあった。しかしこの2019年のWWDCでは、そんな懸念を払拭するように、皆に望まれるような改善が行われたことが、何より嬉しい。UIKit for MacもSwiftUIも、ここから何年かかけてさらに磨き上げられていくことを思えば、楽しみが増すばかりである。