2020年6月2日火曜日

[Swift5, Xcode11] Mac Catalyst で悪戦苦闘

1. archiveできない!?


古いXcodeを使っていた時のくせで、archiveの時は[Generic iOS Device]を選ぶものと思い込んでいた。
でもそれだと、ipaファイルしか生成されない。

【処方箋】正解は、[My Mac]を選んでarchive


2. スワイプできなくねぇ?


SceneKit使っていて、2本指によるスワイプ(位置移動)、ピンチイン・アウト(拡大縮小)ができなかった。ドラッグ(回転)は1本指なのでできる。
ググって見ると、ベータ版の頃から話題になっているよう。重大な不具合に思えるが、現状、直されていないし、制限事項にも上がっていない⁉️

option押しながらのマウス操作で、スワイプ ・ドラッグができるのかと思いきや、なんと用意されていない。
Mac miniとかMac Proでは、入力デバイスとしてタブレットが必要になってしまう。

SceneKit、ARKitを使ったiOSアプリでは、Mac Catalyst対応しました!って能天気にリリースできないという厳しい現実。

【処方箋】別途UIを実装する逃げしかない模様。


3. TableViewも変じゃねぇ?


複数選択(multiple selection)が働かない。

【処方箋】willSelectRowAtとwillDeselectRowAtで、むりやり実装するしか無さそう。

mizuBuro_cureさんの記事が参考になった。

このほかハイライト色(GrayとBlueの差別化)にも問題がありそうなので、複数選択をむりやり実装すると、なんか変な感じになる。
がんばったとしても、開発者のバグをカバーする努力に対して、App Store「アプリ審査」が冷や水を浴びせる可能性が高い。


4. SplitViewの幅おかしくねぇ?


起動時、primary側の幅がせまい。
minimum値を設定しても、それを下回る幅で起動する。

【処方箋】maxとminに同じ値を設定して逃げた。
splitViewController.preferredDisplayMode = .allVisible
splitViewController.minimumPrimaryColumnWidth = 300
splitViewController.maximumPrimaryColumnWidth = 300


5. アプリをクリーンインストールできなくねぇ?


~/Library/Containers/(bundle ID)/Data/Document
~/Library/Containers/(bundle ID)/Data/Library/Application Support/(app)
を参照してるので、アプリケーションフォルダから「通常通り」に削除しても、設定(plistやCoreDataなどなど)は残存してしまう。

【処方箋】Terminalで、defaults delete (Bunndle ID)を実行。


※App StoreでReady for Saleになってから確認したが、正式リリース版では「通常通り」の削除で残存物はなく、クリーンインストールは実現できた。


6. アプリ内課金ができない?


request = SKProductsRequest(productIdentifiers: Set([”XXX.XXX.XXX”]))
request.delegate = self
request.start()

のrequestの永続性が、iOSとは違うみたい。

【処方箋】requestは、SKProductsRequestDelegateを継承しているControllerで保持する。



7. ShareExtensionはむずかしい


私のアプリの作りが悪いのか、NSExtensionPrincipalClassを指定しないと、App Storeに上がらなかった。結果、NSExtensionMainStoryboardは排他の関係となるので、削除した。

MainStoryboardがなくなると、@Objcでクラス名を宣言しないと、OSは呼んでくれなくなる。
例:
@objc(ShareViewController)
class ShareViewController: SLComposeServiceViewController {
...
}


8. 勝手にメニューが作成されてしまう


勝手に作ってくれるのはいいのだけれど、問題だらけ。

1. Bundle display nameが使われない。XXX.appのXXXの部分が使われてしまう。


【処方箋】PRODUCT_NAMEを変えるしかなさそう。


2. File > Preferenceで、Version、Build、Acknowledgementが出てくるが空っぽ。


RunScriptで以下のようなスクリプトを走らせてるんだけど、これじゃぜんぜんダメ。

version=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$PROJECT_DIR/$INFOPLIST_FILE")

build=$CURRENT_PROJECT_VERSION

root=${CODESIGNING_FOLDER_PATH}/Settings.bundle/Root.plist

if test -f $root; then

/usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:1:DefaultValue $version" "$root"

/usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:2:DefaultValue $build" "$root"

fi



【処方箋】以下のようにしてほぼ削除。

extension AppDelegate {

    override func buildMenu(with builder: UIMenuBuilder) {

        super.buildMenu(with: builder)

        guard builder.system == .main else { return }

        builder.remove(menu: .preferences)

        builder.remove(menu: .services)

        builder.remove(menu: .file)

        builder.remove(menu: .edit)

        builder.remove(menu: .format)

        builder.remove(menu: .view)

        builder.remove(menu: .window)

        builder.remove(menu: .help)

    }

}


「Helpがないと審査に通らない」という書き込みも見たような気がするが、とりあえずこれで挑戦してみる。

追記:
Aboutのコピーライトだけは、NSHumanReadableCopyrightで簡単にできた。

2020年5月23日土曜日

[Swift 5, Xcode11, CoreData] UITableViewでSectionタイトルを出す

1. モデルエディタでCoreDataを編集


Sectionタイトルに対応する項目を追加する。
ここでは、sectionName:Stringとし、Default値を「Data」とした。
Transientは任意だが、ONとした。
TransientをONにすると、データベース(sqlite)にカラムは生成されない。


2. コーディング


NSFetchedResultsController作成時に、引数sectionNameKeyPath:で、作成したCoreDataの項目名"sectionName"を渡す。


Sectionタイトル用のtableViewのデリゲートメソッドを使い、.nameを返す。


3. 動作例


Sectionタイトルが現れ、Default値「Data」で分類できた。



2020年3月24日火曜日

[iOS App] フランス語 中国語 ローカライズの効果

1. フランス語のローカライズ


続報です。
一過性ではなく、シェアはそのまま維持されているようです。
さらなる伸びはないですが...


2. 中国語のローカライズ


gengoに700円くらい払って、繁体と簡体のローカライズしてみました。
フランス語よりは効果あるみたいです。
世界シェア的にはもっとあってもいいような気もしますが、中国本土でAppleのアカウント持ってる人(海外に出ることができる限られた富裕層)ってこのぐらいしかいないんですかね。


2020年2月22日土曜日

[iOS App] フランス語 ローカライズの効果

私が配信しているアプリで、唯一のヒット作が Midnight Video Camera


ですが、これを使って多言語化がどれだけ効果があるのか検証してみました。

1. まずは実績


英語をメイン言語にし、日本語を加えて、2年くらい配信しているのですが、アメリカ3:日本7が実体です。イギリス、オーストラリアもあるけど、ほぼアメリカと考えていいようです。


当方としては初めから海外リリースを念頭に取り組んできたので、シェア的には5:5になるべきものが3:7であるという結果は予想外で、原因があれば取り除きたいと考えています。
が、思い当たる節はないんですよね。そもそもアプリ名は英語名しか用意していないし。
App Storeの仕様としか思えないんですがね。

2. フランス語化


買ってくれそうでパイがでかい国となると、ヨーロッパ言語かと思い、手始めにフランス語化してみました。


配信開始月(1月)に明らかに伸びはありましたが、全体(アメリカ+日本)からみると1/50くらいですか。劇的な効果はでませんね。
あとヨーロッパならやってドイツ語か(ブラジルが買ってくれるなら)ポルトガル語もありかと思いますが、やはり中国語ですかね。
App Storeのシェアは、アメリカ:日本:中国=1:1:1らしいんで。

2020年2月11日火曜日

[Xcode11] libstdc++をまだ使いたい

Podで引き込んだライブラリが要求するんで、libstdc++を今でも使う場面があるかと思います。
最終リリースはXcode9なので、そこから抽出する必要があります。

ビルドするための処方箋


1. ヘッダー


/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include
のc++フォルダをまるごと、Xcode11の同ディレクトリにコピー。

2. ライブラリ


/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib
の3つ
libstdc++.6.0.9.tbd (実体)
libstdc++.6.tbd (シンボリックリンク)
libstdc++.tbd (シンボリックリンク)
をXcode11の同ディレクトリにコピー。


iOS Simulatorでデバッグするための処方箋


1. ヘッダー


/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include
のc++フォルダをまるごと、Xcode11の同ディレクトリにコピー。

2. ライブラリ


/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib
の3つ
libstdc++.6.0.9.tbd (実体)
libstdc++.6.tbd (シンボリックリンク)
libstdc++.tbd (シンボリックリンク)
をXcode11の同ディレクトリにコピー。

3. ライブラリ


/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib
の3つ
libstdc++.6.0.9.dylib (実体)
libstdc++.6.dylib (シンボリックリンク)
libstdc++.dylib (シンボリックリンク)
をXcode11の
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib
にコピー。

2019年12月29日日曜日

ipaファイル内のembedded.mobileprovisionから有効期限(ExpirationDate)を読み込む

AdHocやEnterpriseでのリリースでは有効期限があるので、それを取り出し、ユーザーに見せるようにしていた。

embedded.mobileprovisionをNSDataで読み込み、NSStringに全変換してから、NSStringのメソッドで処理していたのだが、暗号化された文字コード以外の値が入っているため、SDK13ではクラッシュを招いてしまった。

反省して、NSPropertyListSerializationで処理する正しい方法に改めた。



-(NSString *)readExpiredDay
{
    NSString *path = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
    if (!path) return @"NG";
    NSData *data = [NSData dataWithContentsOfFile:path];
    
    // plistを抽出
    NSRange start = [data rangeOfData:[@"<?xml" dataUsingEncoding:NSUTF8StringEncoding] options:NSDataSearchBackwards range:NSMakeRange(0, [data length])];
    NSRange end = [data rangeOfData:[@"</plist>" dataUsingEncoding:NSUTF8StringEncoding] options:NSDataSearchBackwards range:NSMakeRange(start.location, [data length] - start.location)];
    NSData *body = [data subdataWithRange:NSMakeRange(start.location, end.location + end.length - start.location)];
    
    // NSDictionaryに変換
    NSPropertyListFormat format;
    NSDictionary *dic = (NSDictionary *)[NSPropertyListSerialization propertyListWithData:body options:NSPropertyListMutableContainersAndLeaves format:&format error:nil];
    if (!dic) return @"NG";
    
    // ExpirationDateを検出
    NSDate *date = dic[@"ExpirationDate"];
    if (!date) return @"NG";

    // UTC->JST変換し文字列とする
    NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
    [fmt setDateFormat:@"yyyy/MM/dd"];
    [fmt setTimeZone:[NSTimeZone defaultTimeZone]];
    NSString *str = [fmt stringFromDate:date];
    
    if (!str) return @"NG";
    return str;
}

swift版のM.Ike様のコードを参考にしました。


[Xcode11 SDK13] UISplitViewControllerでメニューが引っ込まない

Xcode6(SDK8)で非推奨になったのに、放っておいたら、「戻る」ボタンは表示されないし、メニューとして使っているプライマリビューは隠れないし、たいへんなことになってしまった。

willHideViewController:withBarButtonItem:forPopoverController:
のforPopoverControllerでメニューのオブジェクトを取得し、メニューを隠すのに使っていたのだが、SDK13からforPopoverControllerは何も返さなくなってしまった。

修正点は以下の2点。

1. メニューを隠す


縦画面の時だけ隠したいので、端末の向きを、

// NotificationCenter登録


[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changedOrientation:) name:UIDeviceOrientationDidChangeNotification object:nil];

// ハンドラ


- (void)changedOrientation:(NSNotificationCenter*)center {
    switch ([UIDevice currentDevice].orientation) {
        case UIDeviceOrientationLandscapeLeft:
        case UIDeviceOrientationLandscapeRight:
        case UIDeviceOrientationPortrait:
        case UIDeviceOrientationPortraitUpsideDown:
            _orientation = [UIDevice currentDevice].orientation;
            _splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAutomatic;
            break;
        default: //FaceUp及びFaceDownでは何もしない。
            break;
    }
}

で_orientationを取得した上で、今までどおり、ゆっくり隠したいので0.3秒を指定。

// メニューを押した後に、以下を実装


switch (_orientation) {
    case UIDeviceOrientationPortrait:
    case UIDeviceOrientationPortraitUpsideDown: {
        [UIView animateWithDuration:0.3 animations:^{
        _splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryHidden; }];
        break; }
    default:
        break;
}

2. 「戻る」ボタンを表示する


以下のようなものをviewDidLoadに実装するだけで、あとは自動的にやってくれる

navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem;

はずだったのだが、起動時の一発目に、左上の文字がでないことがあった。
白紙から作り直せばうまくいくのだが、そういう訳にもいかないので、viewDidAppearに以下を実装。

navigationItem.leftBarButtonItem.title = @"●●●";