Koichi Adachi

smart phone app development tips

Swift convert Dictionary<Int,AnyObject> to Dictionary<HogeType,AnyObject>

@objc public enum HogeType:Int{
  case unknown
  case hoge1
  case hoge2
  case hoge3
  case hoge4
  case hoge5
}

class A{  
}

bucket brigade

let convertedDict = [1:A(), 2:A(), 100:A()].reduce([HogeType:A]()){
  
  guard let controlType = HogeType(rawValue: $1.key) else {
    return $0
  }
  var bucket = $0
  bucket.updateValue($1.value, forKey: controlType)
  return bucket
}

remove complex code

let convertedDict = [1:A(), 2:A(), 100:A()].map {(HogeType(rawValue:$0.key),$0.value)}.filter{(($0.0) != nil)}

pass a enum key Dictionary to Swift from ObjC

expect receive enum-key dict (Dictionary<EnumType,T>) from Objective-C. but can't receive direct EnumType key because XCode abandon generate method.

ObjC's NSDictionary not allow Int for key. to use instead of NSNumber.

definition in swift

@objc public enum HogeType:Int{
  case unknown
  case hoge1
  case hoge2
  case hoge3
  case hoge4
  case hoge5
}

public class A{
public class func wantToTreatMethod(dictionary:Dictionary<Int, String>){
  //TODO:convert Dictionary<Int, String> to Dictionary<HogeType, String>
  // next article.
}

auto converted in ObjC

typedef SWIFT_ENUM(NSInteger, HogeType) {
  HogeTypeUnknown = 0,
  HogeType1 = 1,
  HogeType2 = 2,
  HogeType3 = 3,
  HogeType4 = 4,
  HogeType5 = 5,
};

SWIFT_CLASS("frameworknamespacehogehoge")
@interface A : NSObject
+ (void)wantToTreatMethod:(NSDictionary<NSNumber *, NSString *> * _Nonnull)dict; //key type is Int in Swift, It was not made this method.
@end

invoke from ObjC

NSDictionary<NSNumber*, NSString*> *dict
     = @{@(HogeTypeHoge1) : @"a",
         @(HogeTypeHoge2) : @"b",
         @(HogeTypeHoge3) : @"c",
         @(HogeTypeHoge4) : @"d",
         @(HogeTypeHoge5) : @"e"};

[A wantToTreatMethod:dict];

Android Studio v2.2.3

Android Studio v2.2.3が出た。
Android Studio Release Notes | Android Studio 2.2との大きな違いは

  • ProGuardで変換した内容がおかしいため、バージョンをProGuard 5.3.1から 5.2.1に戻した。(superの代わりにthisになっていた問題の解消)

ProGuard Java Optimizer and Obfuscator / Bugs / #625 Wrong code generated when MyClass.super.method()

その他の修正内容はここ
Issues - android - Android Open Source Project - Issue Tracker - Google Project Hosting

自作ライブラリをシームレスにObjCからSwiftへ移行する話(iOS Advent Calendar 2016/12/15)

iOS Advent Calendar 2016 - Qiitaの15日目の記事です。

これまでObjCで書いていたライブラリを'少しずつ'Swiftに移行する話です。

自己紹介

フリーランスiOSAndroid、Web上で動くライブラリの開発をしています。 最近はEC2のインスタンス上にSwift Linuxで作った簡単なプロジェクトをDockerに入れて起動してみたり・・・といったことがマイブームです。

動機

仕事に適用する前に、まずは個人プロジェクトで作り続けてきたライブラリプロジェクトをSwift化しようと思ったのがきっかけです。 WWDC16のラボで聞いたところ、「iOS8以上だったらSwiftで.frameworkを書けるよ!」とAppleのエンジニアの方にお聞きしたため これはチャンス!と思いやってみました。

このトピックで実現すること

  • 既存ライブラリ(ObjC)のプロジェクトはそのまま残す。
  • Swiftで書きたい(移行したい)ロジックから新規ライブラリプロジェクトに記述(移動)する
  • AppからはSwiftライブラリとObjCライブラリの両方のクラスを呼び出せるようにする

制約

  • この構成ではObjCライブラリ<->Swiftライブラリの相互参照はできないため、  Swiftライブラリ->ObjCライブラリという参照関係になる
  • iOSのDeployment Targetは8.0以上

省略しているところ(このトピックでの本質的な話ではないため)

  • bitcodeをオンにする(Build Settingsの3項目)
  • Swift<->ObjCのbridging headerとか
  • アプリがObjCプロジェクトだったらAlways Embed Swift Standard LibrariesをYESにする

初期状態

f:id:sgspecial:20161214223929p:plain

  • workspaceで構成されていること
  • ObjCで作られたframework(以下ObjCFramework)を参照している

ステップ1 新規作成したSwiftコード用frameworkをworkspaceに追加

f:id:sgspecial:20161214223958p:plain

SwiftFrameworkのBuildPhaseに、ObjCFrameworkへの参照を追加する

f:id:sgspecial:20161214224034p:plain

アプリのターゲットのBuild Phase内、「Link Binary With Libraries」と「Embed Framework」にSwiftFrameworkへの参照を追加する

f:id:sgspecial:20161214224100p:plain

アプリのコードからそれぞれのロジックを呼んでみる

f:id:sgspecial:20161214224139p:plain

動いた! f:id:sgspecial:20161214224150p:plain

これでアプリからもSwiftで作ったFrameworkからもObjCで作ったライブラリを参照できた。

f:id:sgspecial:20161214224201p:plain イメージ

ただ、これだとframeworkを2つ参照しなければならない。それは面倒。

SwiftFrameworkにframeworkを同梱できたらいいのに。

frameworkを消して「Runpath Search Paths」と「Framework Search Path」に記述を追加

f:id:sgspecial:20161214224221p:plain Runpath Search Pathには
@executable_path/Frameworks/UmbrellaSwiftFramework.framework/Frameworks

f:id:sgspecial:20161214224235p:plain Framework Search Pathには
$(PROJECT_DIR)/UmbrellaSwiftFramework.framework/Frameworks

動いた!

けどiTunesConnectにプッシュしてみるとエラーが出る。 hogehoge.frameworkにFrameworkフォルダやbundleが入っているとiTunesConnectにアップできないようです。

f:id:sgspecial:20161214224305p:plain

Releaseビルドではフォルダを変更する。

iTunesConnectへのアップ時にパスするフォルダ構成にすればいいだけなので、 下記のスクリプトをアプリの「Run Script」に追加する。

if [ "${CONFIGURATION}" != "Release" ]; then
exit;
fi
mkdir ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ObjCFramework.framework
mv -f ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/UmbrellaSwiftFramework.framework/Frameworks/ObjCFramework.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}
rm -rf ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/UmbrellaSwiftFramework.framework/Frameworks

f:id:sgspecial:20161214224320p:plain

成功!

まとめ

アプリを作る時にはよくライブラリを作ると思いますが、
一気にSwift化するのがつらい時にこの手の方法が使えるかと思います。
個人開発者でも仕事で作っているアプリでも、優先順位やビジネスへのインパクトなど様々な事情があります。
全ての開発者が必ずしも 全てのコードをSwift化する 工数を確保できるとは限りません。 (自分) そのような時、部分的にでもSwift化できればいいかなと思い調べてみました。

特にAndroid(Java, Kotlin)やWeb(JS)などで同時開発をしたい時に、ObjCよりはソースコードの比較がしやすくなるため、
このような形でSwiftへ移行するのはありなのではないでしょうか。
どなたかのお役に立てれば幸いです。

SwiftでHogehoge.frameworkを作る

Xcode8betaが出たので、気になっていた「Swiftでコードを書いて.frameworkを作ることができるのか」を試してみました。 結論からいうとできるみたい。

検証したこと

  • DeploymentTargetはXcode8.0betaの下限のバージョンであるiOS8.0を指定。
  • Swiftで書いたライブラリコードを、アプリのSwiftコードから呼びだし
  • Swiftで書いたライブラリコードを、アプリのObjective-Cコードから呼びだし
  • Objective-Cで書いたライブラリコードを、アプリのSwiftコードから呼びだし
  • Objective-Cで書いたライブラリコードを、アプリのObjective-Cコードから呼びだし

これらすべてOKだった。

手順。 1. HogeHogeAppという名前でアプリを作成 1. MyFrameworkという名前で、CocoaTouchFrameworkを作成 1. それらをまとめるworkspaceを作成(ここではSandboxSwiftFramework.xcworkspaceとした) 1. 3に1と2を入れる

Swiftで書いたコードをライブラリに追加(外から使うにはpublicが必要)

import Foundation

public class StringUtil : NSObject{
    public class func getHoge()->String{
       return "hoge"
    }
    
    public func getFuga()->String{
        return "fuga"
    }
}

もちろん、既存のライブラリコードはObjective-Cで存在することもあるため、 ObjCのライブラリコードも試してみる

ObjCで書いたライブラリコードを追加

#import <Foundation/Foundation.h>

@interface StringEx : NSObject

+(NSString*)getHoge;
-(NSString*)getFuga;

@end
@implementation StringEx

+(NSString*)getHoge{
    return @"fuga";
}

-(NSString*)getFuga{
    return @"Hoge";
}
@end
  • 単純にObjCのファイルを追加しただけだと、外から呼び出せないため、headerをpublicに変更
  • StringEx.hのインポート文を、標準で作られるMyFramework.hに追記

f:id:sgspecial:20160618170912p:plain

//  MyFramework.h

#import <UIKit/UIKit.h>

//! Project version number for MyFramework.
FOUNDATION_EXPORT double MyFrameworkVersionNumber;

//! Project version string for MyFramework.
FOUNDATION_EXPORT const unsigned char MyFrameworkVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <MyFramework/PublicHeader.h>


#import "StringEx.h"

次はアプリ側。

MyFrameworkをアプリから呼べるように、アプリ側のBuildSettingsを以下のように変更。

f:id:sgspecial:20160618172332p:plain

アプリ側のSwiftコードからライブラリのクラスを読み込み

import UIKit
import MyFramework

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        print("StringUtil " + StringUtil.getHoge())
        print("StringUtil " + StringUtil().getFuga())
        print("StringEx " + StringEx.getHoge())
        print("StringEx " + StringEx().getFuga())
        
    }   
}
#import "SecondViewController.h"
@import MyFramework;

@interface SecondViewController ()

@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    
    NSLog(@"StringUtil %@", [StringUtil getHoge]);
    NSLog(@"StringUtil %@", [[[StringUtil alloc] init] getFuga]);
    NSLog(@"StringEx %@", [StringEx getHoge]);
    NSLog(@"StringEx %@", [[[StringEx alloc] init] getFuga]);
    
}
@end

実行するとこんな感じ。下

StringUtil hoge
StringUtil fuga
StringEx fuga
StringEx Hoge
2016-06-18 01:25:26.660 HogeHogeApp[8712:2887276] StringUtil hoge
2016-06-18 01:25:26.662 HogeHogeApp[8712:2887276] StringUtil fuga
2016-06-18 01:25:26.663 HogeHogeApp[8712:2887276] StringEx fuga
2016-06-18 01:25:26.663 HogeHogeApp[8712:2887276] StringEx Hoge

感想

WWDC16に参加中にXcode8.0betaを落としたところ、なんとDeploymentTargetの下限が8.0になっています。 「もしかしたらDynamicLinkLibraryをSwiftで作成できるのでは?」という疑問から試してみたところ、あっさりできました。 せっかくなのでラボ確認したところ、DynamicLinkLibraryはiOS7では対応しておらず、iOS8からは公式にサポートしているとのこと。

iOS7だとDynamicLinkLibraryを作ることができず、staticライブラリだとswiftでライブラリ作れないよーとXcodeに言われてしまって、いままでswiftでバイナリ配布型のライブラリは作ることができませんでした。 (オープンソースのようにソースコード配布ができるなら問題にならないですが)

これでSwiftで書いたライブラリを綺麗に管理できるようになるなーと、テンションが上がったラボでした。

追記

Xcode8.0betaのバグなのか、NSLogはエミュレーターではログを吐かない。

Scala defの引数にクラスを指定する

noneだったら指定のエラーのLeftに入れて返すメソッドを作った時に、classを渡せないかなーと思い、こんな風に書いてみた。 このメソッドは作らずにも実現できることが分かったので、使わなくなったけど、メモ。

  private def noneSomeFilter[M](maybeObject: Option[M], clazz: Class[_ <: Any]): \/[ErrorReason, M] = {
    maybeObject match {
      case Some(m) => m.right
      case _ => new ErrorReason(clazz).left
    }
  }

Adobe Airの環境構築 on Mavericks

詰まったのでメモ。

Flash Builder 4.7は、 Javaのバージョンが1.6でないと動かないらしい。 iOS用のプロジェクトを動かそうとすると Exception in thread "main" java.lang.Error: Unable to find llvm JNI lib in: /Applications/Adobe Flash Builder 4.6/sdks/4.6.0/lib/adt.jar/Darwin /Applications/Adobe Flash Builder 4.6/sdks/4.6.0/lib/aot/lib/x64 /Applications/Adobe Flash Builder 4.6/sdks/4.6.0/lib/adt.jar /Applications/Adobe Flash Builder 4.6/sdks/4.6.0/lib

      at adobe.abc.LLVMEmitter.loadJNI(LLVMEmitter.java:572)
      at adobe.abc.LLVMEmitter.<clinit>(LLVMEmitter.java:585)
      at com.adobe.air.ipa.AOTCompiler.generateExtensionsGlue(AOTCompiler.java:516)
      at com.adobe.air.ipa.AOTCompiler.generateMachineBinaries(AOTCompiler.java:1151)
      at com.adobe.air.ipa.IPAOutputStream.createIosBinary(IPAOutputStream.java:284)
      at com.adobe.air.ipa.IPAOutputStream.finalizeSig(IPAOutputStream.java:599)
      at com.adobe.air.ApplicationPackager.createPackage(ApplicationPackager.java:90)
      at com.adobe.air.ipa.IPAPackager.createPackage(IPAPackager.java:217)
      at com.adobe.air.ADT.parseArgsAndGo(ADT.java:557)
      at com.adobe.air.ADT.run(ADT.java:414)
      at com.adobe.air.ADT.main(ADT.java:464)

こんな感じのエラーが出て先へ進めない。

・リンクからjdk1.6をインストール。  Java for OS X 2014-001  →/System/Library/Java/〜辺りにインストールされる。 ・JAVA_HOMEに↑を設定 ・/Library/Java/JavaVertualMachine/jdk1.7xx系を削除 ・再起動 ・Flash Builder 4.7で、JavaのHOMEを↑に設定。

無事に実機で動いた。