頭と尻尾はくれてやる!

パソコンおやじのiPhoneアプリ・サイト作成・運営日記


MTLTextureをpngファイルで保存するとなぜかサイズ(w, h)が大きくなる

サイズがw x hのMTLTextureがあるとする。
これを以下のようにしてpngで保存する。
let ciImage = CIImage(mtlTexture: mtlTexture, options: nil)!
let uiimage = UIImage(ciImage: ciImage)
let data = uiimage.pngData()!
try! data.write(to: url)


保存したpngファイルのサイズを確認すると(w x 3 , h x 3)になってる。
3倍になってるのは私のiPhone 12 Pro Maxの場合で、これがiPadだと(w x 2, h x 3)なのでUIScreen.main.scaleの値が効いてるのかもしれない。ともかく2倍なり3倍で保存されるのは具合が悪い。

ios - Saving and Loading MTLTexture as PNG - Stack Overflow
↑同じような感じで困ってる人の投稿があったが回答はなし、、、(よくあるよね)

MTLTextureから上記の処理で作成したUIImageオブジェクトにはCGImageがない。
uiimage.cgImageはnilだ。

なのでCGImageを作成してみた。

cocoa - CGImage from byte array - Stack Overflow

↑この記事の下の方にSwiftでのコードがあるので参考にした。

MTLTextureからデータ配列を作成するには
getBytes(_:bytesPerRow:from:mipmapLevel:)
を使用。
コードはこんな感じになった↓
    private func makeCGImageFromMTLTexture(_ mtlTexture: MTLTexture) -> UIImage {

let width = mtlTexture.width
let height = mtlTexture.height
let numComponents = 4
let numBytes = height * width * numComponents

let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0),
size: MTLSize(width: width, height: height, depth: 1))
var pixelData: [UInt8] = [UInt8](repeating: 0, count: numBytes)
mtlTexture.getBytes(&pixelData, bytesPerRow: width*4, from: region, mipmapLevel: 0)

let colorspace = CGColorSpaceCreateDeviceRGB()
let rgbData = CFDataCreate(nil, pixelData, numBytes)!
let provider = CGDataProvider(data: rgbData)!
let cgImage = CGImage(width: width,
height: height,
bitsPerComponent: 8,
bitsPerPixel: 8 * numComponents,
bytesPerRow: width * numComponents,
space: colorspace,
bitmapInfo: CGBitmapInfo(rawValue: 0),
provider: provider,
decode: nil,
shouldInterpolate: true,
intent: CGColorRenderingIntent.defaultIntent)!

return cgImage
}


このメソッドを使い、同様にpngファイルで保存を試みた。
let cgImage = makeCGImageFromMTLTexture(mtlTexture)
let uiimage = UIImage(cgImage: cgImage)
let data = uiimage.pngData()!
try! data.write(to: url)

すると無事にサイズは3倍などにはならずに意図通りのサイズで作成できた。

しかし、、、色がおかしい!

MTLTexture から CGImage を生成 - Qiita

↑調べたらこちらの記事と同じことかな、と思い入れ替える処理を実装すれば無事に正しい色で表示された!
その場合のメソッドは以下のような感じ。
    import Accelerate
private func makeCGImageFromMTLTexture(_ mtlTexture: MTLTexture) -> UIImage {

let width = mtlTexture.width
let height = mtlTexture.height
let numComponents = 4
let numBytes = height * width * numComponents
let rowBytes = width * numComponents

let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0),
size: MTLSize(width: width, height: height, depth: 1))
var bgraBytes = [UInt8](repeating: 0, count: numBytes)
mtlTexture.getBytes(&bgraBytes, bytesPerRow: width*4,
from: region, mipmapLevel: 0)

var bgraBuffer: vImage_Buffer = vImage_Buffer()
bgraBytes.withUnsafeMutableBufferPointer {bgraBytesBP in
bgraBuffer = vImage_Buffer(data: bgraBytesBP.baseAddress,
height: vImagePixelCount(height),
width: vImagePixelCount(width),
rowBytes: rowBytes)
}

var rgbaBytes = [UInt8](repeating: 0, count: numBytes)
var rgbaBuffer: vImage_Buffer = vImage_Buffer()
rgbaBytes.withUnsafeMutableBufferPointer {rgbaBytesBP in
rgbaBuffer = vImage_Buffer(data: rgbaBytesBP.baseAddress,
height: vImagePixelCount(height),
width: vImagePixelCount(width),
rowBytes: rowBytes)
}

let map: [UInt8] = [2, 1, 0, 3]
vImagePermuteChannels_ARGB8888(&bgraBuffer, &rgbaBuffer, map, 0)


let colorspace = CGColorSpaceCreateDeviceRGB()
let rgbData = CFDataCreate(nil, rgbaBytes, numBytes)!
let provider = CGDataProvider(data: rgbData)!
let cgImage = CGImage(width: width,
height: height,
bitsPerComponent: 8,
bitsPerPixel: 8 * numComponents,
bytesPerRow: rowBytes,
space: colorspace,
bitmapInfo: CGBitmapInfo(rawValue: 0),
provider: provider,
decode: nil,
shouldInterpolate: true,
intent: CGColorRenderingIntent.defaultIntent)!

return cgImage
}

スポンサーサイト






Swiftでポインタがdangling pointerだと警告が出る

Swiftでポインタを扱うと
Initialization of 'UnsafeMutableRawPointer' results in a dangling pointer
なんて警告をよく見てた。
とりあえずコードは動くのだが気持ち悪いのでやはりリリース用には使いたくない。

今回もdangling pointerだぞ!と出たので調べたら

Inout expression creates a tempora… | Apple Developer Forums
↑この記事が出てきた。
あああ、そんな記述ができたのか、ということで無事警告を回避できた。


Swiftでクラスのオブジェクトをコピーする

SwiftでクラスのオブジェクトをコピーするにはNSCopyingプロトコルに準拠させて
func copy(with zone: NSZone? = nil) -> Any
↑このメソッドを書くのが基本みたいなんだが、、、コピーしたいクラスには変数がいっぱいあって、しかも今後増えるかもしれない。
その時にここで代入を忘れたら?オプショナル型だと代入忘れも気付かない?などと不安になる。

どうしたもんかなあと思ったが、元々そのコピーしたいクラスが保存のためNSSecureCodingプロトコルに準拠させてた。
なので、試しにUserDefaults使って保存して、すぐ取り出して別オブジェクトとしてみた。
class Person: NSObject,  NSSecureCoding {

func copyWithUserDefaults() -> Person {

let userDefaults = UserDefaults()
let key = "Person_data"

//save
let data = try! NSKeyedArchiver.archivedData(withRootObject: self,
requiringSecureCoding: true)
userDefaults.set(data, forKey: key)

//load
if let data = userDefaults.object(forKey: key) as? Data {
let classes = [Person.self]
if let person = try! NSKeyedUnarchiver.unarchivedObject(ofClasses: classes,
from: data) as? Person {
userDefaults.removeObject(forKey: key)
return person
}
}
abort()
}
↑コピーする部分のコード(do/try/catchとかは略してる)。
実行すると意図通りコピーできたけど、、、なんか邪道な気がする、、、(-_-;



macOS 10.15.7 Catalina
Xcode 12.0.1
Swift 5.3


App Clipに対応する

リリースしてる「わざタイプ相性表(for ポケモンGO)」というiOSアプリがあって、これをApp Clip対応してみようと試みた。


App Clip用のコードは元のプロジェクトに別ターゲットで作成する。

App Clipのテンプレート

↑この時App Clipのテンプレートがあるはず。

テンプレのおかげでほとんど必要なややこしい設定はしてくれそうなもんだが、自分で設定しなければいけないのもある。
まずAssociated Domainsなるもので、参考ページ(1)のAdd the Associated Domains Entitlementに書いてるようなことをする必要がある。大事なのは
you must add the Associated Domains Entitlement to the app and the App Clip target:
とあるように、元のappとApp Clipの両方のtargetで作成する必要がある。
これを最初見落としてApp Clip側だけに作成したので、わけのわからないエラーが出て散々回り道をしてしまった(両方に設定しなさい、なんて素直なエラーは出ない)。


最初はApp Clip側のtargetには何もないだろうから必要なファイルをApp Clip側のtargetに追加するなどする。
これを実機にビルドすると普通に起動するのでApp Clip実行時の動作確認ができる。

自分のホームページに必要なタグを設定すると、スマートバナーと呼ばれるApp Clipへのリンクが表示される。

スマートバナー参考ページ

↑これはあるアプリのサイトで、上部にスマートバナーが表示されてる。しかしこれはリリースしないとダメっぽいので事前に確認はできない。

しかしinvocation URLを入れたQRコードを使えばApp Clip Cardというものがディスプレイ下部に表示される。
実際にQRコードは使わないだろうけど、App Store Connectで入力しないといけないし事前にどう表示されるのか確認したい。
それにはiPhoneの設定アプリにあるデベロッパ(初めて使ったよ)って項目にあるAPP CLIPS TESTINGというところを設定する。
ここで適当な画像やURL、BUNDLE IDを設定し、QRコードを読み取ると、、、

テスト用のApp Clip Card

↑このように画面下部にApp Clip Cardが表示される(ここでPokeGoChartと表示されてるのはbundle display nameを設定してなかったのでプロジェクト名が表示されてる)。
開くをタップするとすでにビルドしてたApp Clipが起動した。

以上の流れでApp Clip、App Clip Cardの動作を確認できた。なお、ここまでTest Flightは使っていない。

なお、カメラでQRコードを読み取る時にはカメラアプリではなくコントロールセンターにある(なければ設定で追加する)QRコードを読み込むカメラアプリでないとダメだったということ。この情報にたどり着くまでにかなりはまった。

時系列に書くけど、、、App Clip、App Clip Cardの動作確認ができたのでここでアプリのアップデートを申請することにした(ホントはまだ足りない!けど後述のようになんとかなる)。

1件の無効なドメイン

↑申請の準備中にApp Store Connectでビルド(自分が送信したファイルを確認するところ)のドメインステータスのところに「1件の無効なドメイン」とエラーが出てる。なんだかよくわからなかったがダメならエラーメッセージが出るだろうと申請ボタンを押したら申請できてしまった。
その日の深夜に審査に入り、審査を通過した。

しかし、何をやってもApp Clipをダウンロードできない、、、

調べるとどうやらapple-app-site-associationなるものをサーバにアップしないとダメらしい(参考ページ(2)にある)。
細かい解説は参考ページ(3)にあるのだが、

apple-app-site-associationのフォーマット

↑ファイルはこのように置いてね、とある。これを見た時には世の中に .well-known という名前のよく使われるディレクトリが存在する(試しにググったら色々用途があることがわかる)と知らなかったので、よくあるexample.comみたいに自分の環境で該当するのを割り当てる文字列だと思ってた。なので最初は一番上の https://ringsbell.net にファイルを置いた。すると、、、

ドメインURLステータス

↑デバッグステータスだけは緑になった(間違ってるんだから変わるなよ!と後から思うが)。
しかし、これ以降いくら待ってても変化がないので調べると、上記のように .well-known ってディレクトリに置くことを知ってそのようにした。

ドメインステータス

↑するとその3時間弱ほど経った後にキャッシュの方も緑になっていた!(気付いたのはもっと後だったけど)

スマートバナー表示

↑その気付いた時(緑になってから2時間後)、iPadのカメラでQRコードを読むとこの画面に遷移。上部にApp Clipへのリンクが出てる!開くをタップすると

App Clip Card表示

↑App Store Connectで指定したApp Clip Cardが表示された!もちろん開くをタップでApp Clipが表示された(ダウンロード時にlaunch screenが表示されるのかと思ったがそうでもない?)。

しかし、なぜかiPhoneでは従来通りApp Clipを認識しない、、、iPhoneを再起動しようともSafariやApp Clipのキャッシュを消そうともダメだった。iOSとiPadOSの違いだとどうしようもないか、iOSの次のアップデートまで待たないとダメなのか、、、?と諦めてたが翌日の15時頃にはiPhoneでも意図通り動いてくれた。なんとも不安定だな、、、

メッセージでの表示

↑なお今のところメッセージでURLを送信してもSafariへのリンクしか表示されない(画像とか表示されるのでは?)。


おまけ
QRコード
↑QRコードはこちら。iPhoneなどのカメラでQRコードを認識するとスマートバナーが見られると思います。
このURLはhttps://ringsbell.net/ios/pokegochart です。アプリの紹介ページですが、iOSのSafariで見ると上部にスマートバナーが表示されるはずです。
わざタイプ相性表 for ポケモンGO



参考
(1)Creating an App Clip with Xcode | Apple Developer Documentation
(2)Configuring Your App Clip’s Launch Experience | Apple Developer Documentation
(3)Supporting Associated Domains | Apple Developer Documentation
(4)QRコード作成はいつもこちらでお世話になってます↓
QRコード作成【無料】アイコン・文字入りQRコード


iOS 14.0.1
macOS 10.15.7 Catalina
Xcode 12.0.1
Swift 5.3



macOSでオブジェクトをクリップボードに入れる

macOSでコピー・ペーストのために文字列とかではなく自作クラスのオブジェクトをクリップボードに入れたい。
文字列のサンプルなら簡単に見つかるのだがオブジェクトはどうするのがベストプラクティスなのか書いてる今もよくわからない。
現時点で動いたコードはおおよそこんな感じ。
@IBAction func copy(_: Any)  {// コピー(⌘+c)でコールされる

let person = Person(withName: name, age: age)//(1)
let data = try! NSKeyedArchiver.archivedData(withRootObject: person,
requiringSecureCoding: true)//(2)

let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setData(data, forType: .string)//(3)
}

@IBAction func paste(_: Any) {// ペースト(⌘+v)でコールされる

let pasteboard = NSPasteboard.general
if let data = pasteboard.data(forType: .string) {//(4)
let classes = [Person.self]
if let person = try! NSKeyedUnarchiver.unarchivedObject(ofClasses: classes,
from: data) as? Person {
print("pasted object, name=\(person.name!), age=\(person.age!)")
}
}
}
まずクリップボードに入れるところ。
(1)入れるのはPersonクラス(よくあるよね)のオブジェクトとする。
(2)このクラスはNSSecureCodingプロトコルに準拠させてる(ここでは詳細略)のでData型に変換できる。
(3)setData(_:forType:)でdataをクリップボードに入れる。ここでのタイプに何を指定すればいいのかわからず、とりあえず.stringとしてる。

次にペーストしてクリップボードの中身を取り出すところ。
(4)NSPasteboardオブジェクトからDataオブジェクトを取り出す。ここでもタイプは.stringとしてる。
後は元のPersonオブジェクトに変換してる。ここでは簡略化のためにtry!としてるけどちゃんとdo/catchしないとカラの時にペーストすると落ちるかもしれない。

一応これで、動いたのだが、、、(3)、(4)で指定するタイプをここでは.stringとしてるが.pngでも動いた。ただしコピー側とペースト側を揃えておけばだが。リファレンスを読んでもわからない、、、
あと、リファレンスには
pasteboard.declareTypes([.string], owner: nil)
↑が必要とあるけどなくても動く、、、



macOS 10.15.6 Catalina
Xcode 11.7
Swift 5.2.4




Copyright ©頭と尻尾はくれてやる!. Powered by FC2 Blog. Template by eriraha.