- 投稿日:2021-01-27T23:01:16+09:00
【React Native iOS】外部アプリからファイルを開く方法
※注意:この記事はiOSのことだけ書いてます。
先に結論
- Document Typesを設定する
- Linkingを使ってファイルパスを取得する
以上。
何がしたいのか
個人でReact Nativeを使ってアプリ開発をしている中で、他のアプリからファイルを受け取る必要があり、今回実装してみました。
正確になんて言うのか、言葉はわからないのですが……これです?
共有シートから、特定のアプリでファイルを開くヤツ。
メールやSafari、AirDropで共有されたファイルも開けるようになります。……なんて言うのが正しいの?
実装環境
- React Native 0.63.4
- XCode 12.3
なおRNプロジェクトの作成方法は割愛します。
? 外部アプリからファイルを取得する
1. 受け取るファイルの種類を設定する
初っ端からXcodeを開きます。
Projectを開いたら、Target < [APP名] < Info < Document Typesを開いて、アコーディオン内の+ボタンを押してください。
- Nameは適当に、ファイル形式の種類の名前を入れます。
Typeには、予め決められているMIMEのようなものを入れます。正しくは
Uniform Type Identifier (UTI)と言うらしいです。
必要な値はここから探せます。その他、独自形式も定義できるみたいです。
Handler Rankは指定した形式のファイルに対して、閲覧者なのか作成者なのかを宣言して、他のアプリとのランク付けをするもの、らしい、です。
値 意味 Owner 指定した種類のファイルを作成します Default 指定した種類のファイルを開きます Alternate 指定した種類のファイルのセカンダリビューアー None 指定した種類のファイルに対して何もしない 2. 受け取ったファイルをRNに送るためのコードを書く
次に、受け取ったファイル(正しくはファイルのパス)を、React NativeのJavaScript側に送るためのコードを書きます。
コードを書くファイルは、AppDelegate.mです。
追加するコードは、(大分わかりにくいですが……)以下の緑色のコードです。
上の方でReact/RCTLinkingManager.hをインポートして、あとは下の方で3行だけコードを入れれば、ひとまず完成です。AppDelegate.m#import "AppDelegate.h" #import <React/RCTBridge.h> #import <React/RCTBundleURLProvider.h> #import <React/RCTRootView.h> + #import <React/RCTLinkingManager.h> ~~~中略~~~ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ~~中略~~ } + - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { + return [RCTLinkingManager application:application openURL:url options:options]; + }3. JS側でファイルパスを受け取る
残るは、ReactNative側でファイルパスを受け取るだけです。
コードから予想できた方も多いと思いますが、URL Schemeで使うLinkingを使ってパスを取得します。
というか2と3に関しては、ドキュメントそのままです。const handler = (filePath: string) => { // 処理内容 }; useEffect(() => { // アプリ起動前に呼ばれる Linking.getInitialURL().then((filePath) => { if (filePath) handler(filePath); }); // アプリ起動後に呼ばれる Linking.addEventListener('url', (event) => handler(event.url)); }, []);URL Schemeで取得するURLの多くは、
https://~~やmailto:~~だと思いますが、ここで取得されるパスはfile:///~~~になっていて、iOS内部の絶対パスが渡ってきます。4. ファイルパスを使って処理する
ここに関しては、必要に応じて処理をしてください。
私はreact-native-fsを導入してreadFileで読み込みました。? LSSupportsOpeningDocumentsInPlaceと共存させる方法
iOS11から追加されたファイル.appから、各アプリのディレクトリにアクセスできるヤツ。
これの有効に必要な
LSSupportsOpeningDocumentsInPlaceをオンにしていると、上記の方法で取得したファイルパスを使っても、No such file or directoryと怒られてしまう場合があります。
LSSupportsOpeningDocumentsInPlaceを有効にすると、ファイルをコピーせず直接オリジナルのファイルを参照しにいきます。
iOSはサンドボックスの制限が厳しいので、(正確な理由はわからんですが)おそらくJS側からオリジナルのファイルがあるパスにアクセスする権限がないのが原因かなと。回避策として、tmpディレクトリにファイルをコピーしてしまう事にしました。
AppDelegate.m- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { NSString* urlPath = url.path; if(![[NSFileManager defaultManager] isReadableFileAtPath:urlPath]) { if([url startAccessingSecurityScopedResource]) { NSString* destPath = [NSString stringWithFormat:@"%@/%@", NSTemporaryDirectory(), [url.path lastPathComponent]]; if(![[NSFileManager defaultManager] copyItemAtPath:urlPath toPath:destPath error:nil]) { [[NSFileManager defaultManager] removeItemAtPath:destPath error:nil]; [[NSFileManager defaultManager] copyItemAtPath:urlPath toPath:destPath error:nil]; } [url stopAccessingSecurityScopedResource]; NSURL* destURL = [NSURL fileURLWithPath:destPath]; return [RCTLinkingManager application:application openURL:destURL options:options]; } } return [RCTLinkingManager application:application openURL:url options:options]; }読み取り権限がないファイルパスだった場合のみ、アプリのtempDirにファイルをコピーして、RNにコピーしたファイルのパスを渡すように変更しました。
これで、JS側からもファイルを読みに行くことができました?〆
普段Web言語ばかり扱っているので、こうしてネイティブ部分のコードに触れて理解できるようになると楽しくなってきますね。
時間を作って、Obj-CやSwiftもしっかり勉強したいと思いました。なおネイティブアプリをまともに書いた経験が無いので、間違いなどあればご指摘ください?♂️
- 投稿日:2021-01-27T19:16:12+09:00
RxSwift6 の変更点まとめ
RxSwiftの6.0が公開されたので変更点をサンプル付きでまとめました。
GitHubのリリースノートやDEV Communityでもまとめられています。
dynamicMemberLookupを使用したバインダーの自動合成今ままでは以下のような
ExampleViewがあったときclass ExampleView: UIView { var text: String }
Binderを毎回記述することでextension Reactive where Base: ExampleView { var text: Binder<String> { Binder(base) { base, title in base.text = text } } }このように
streamをクラスにbindすることができました。let exampleView = ExampleView() Observable.of("text").bind(to: exampleView.rx.text)
RxSwift 6からは、どのクラスに対しても、Binderを自動的に合成します。そのため、上記のBinderのコードをすべて削除してコードをすっきりさせることができるようになりました。
Infallibleエラーを流さない
Observableです。Infallible<String>.create { observer in observer(.next("next")) observer(.completed) // これはコンパイルエラー // observer(.error(NSError())) return Disposables.create {} }
Observableと比べると微妙にメソッドが違います。Observable<String>.create { observer in observer.on(.next("next")) observer.on(.error(NSError())) observer.on(.completed) return Disposables.create {} }また RxCocoa の
DriverとSignalは常にMainSchedulerを使用しリソースを共有するのに対して、Infallible、基本的にObservableである点が異なります。
drive()とemit()で複数のobserverやrelayにバインドできるようになりましたlet myButton = UIButton() Driver.of(true).drive(myButton.rx.isEnabled, myButton.rx.isSelected) Signal.of(true).emit(myButton.rx.isEnabled, myButton.rx.isSelected)
decode(type:decoder:)が追加されましたCombineと同様に、Dataを出力するObservableに対して動作するデコード演算子が追加されました。
service .fetchJSONUsers() // Observable<Data> .decode(type: [Example].self, decoder: JSONDecoder()) // Observable<[Example]>
SingleがSwiftのResultを使うようになりました。subscribe の命名が変わったため警告が出るようになりました。
// RxSwift 5 // 'subscribe(onSuccess:onError:onDisposed:)' is deprecated: renamed to 'subscribe(onSuccess:onFailure:onDisposed:)' single.subscribe( onSuccess: { value in print("Got a value: \(value)") }, onError: { error in print("Something went wrong: \(error)") } ) // RxSwift 6 single.subscribe( onSuccess: { value in print("Got a value: \(value)") }, onFailure: { error in print("Something went wrong: \(error)") } )
ReplayRelayが追加されました新しく
ReplaySubjectをラップしたReplayReplayが追加されました。
BehaviorRelayやPublishRelayと同様にエラーを流しません。
withUnretainedが追加されましたいままでは
weakキーワードを使い循環参照をせずにクラスに値を渡していました。viewModel.importantInfo .subscribe(onNext: { [weak self] info in guard let self = self else { return } self.doImportantTask(with: info) }) .disposed(on: disposeBag)RxSwift 6 からは
withUnretainedを使うことで綺麗に記述することができます。viewModel.importantInfo .withUnretained(self) // (self, info) のタプルになる .subscribe(onNext: { owner, info in owner.doImportantTask(with: info) }) .disposed(by: disposeBag)
distinctUntilChange(at:)で Key Paths をつかるようになりました。Observable.of(Example(text: "1"), Example(text: "2")).distinctUntilChanged(at: \.text) struct Example { let text: String }
DisposeBagのビルダー関数でSwiftUIのようにカンマを使わずに記述することができます。var disposeBag = DisposeBag { observable1.bind(to: input1) observable2.drive(input2) observable3.subscribe(onNext: { val in print("Got \(val)") }) } disposeBag.insert { observable4.subscribe() observable5.bind(to: input5) }
ignoreElementsがObservable<Never>を返すようになりました※ 修正前
public func ignoreElements() { -> Completable { return self.flatMap { _ in return Observable<Never>.empty() } .asCompletable() }※ 修正後
public func ignoreElements() { -> Observable<Never> { self.flatMap { _ in Observable<Never>.empty() } }
UIApplicationにRxのExtensionが追加されました以下変数が追加されました
didEnterBackgroundwillEnterForegrounddidFinishLaunchingdidBecomeActivewillResignActivedidReceiveMemoryWarningwillTerminatesignificantTimeChangebackgroundRefreshStatusDidChangeprotectedDataWillBecomeUnavailableprotectedDataDidBecomeAvailableuserDidTakeScreenshot命名変更
メソッドのリネームがありました。
RxSwift 5 RxSwift 6 catchError(_:)catch(_:)catchErrorJustReturn(_:)catchAndReturn(_:)elementAt(_:)element(at:)retryWhen(_:)retry(when:)takeUntil(_:)take(until:)takeUntil(behavior:_:)take(until:behavior:)takeWhile(_:)take(while:)takeWhile(behavior:_:)take(while:behavior:)take(_:)take(for:)skipWhile(_:)skip(while:)takeUntil(_:)take(until:)observeOn(_:)observe(on:)subscribeOn(_:)subscribe(on:)前のメソッドを使ってもエラーにはならずに以下のような警告が表示されます。
'catchError' is deprecated: renamed to 'catch(_:)'
- 投稿日:2021-01-27T01:23:24+09:00
ViewcontrollerからTabBarControllerへコードで移動する
ViewControllerからTabBarControllerに移動する
まず、最初に下の図のようにViewControllerからTabBarControllerに紐づいてた緑のViewControllerに遷移しても、ViewControllerへの移動なので、
下のタブが表示されません。だから、TabBarViewControllerへ移動する必要があります。
コード
ViewController.swiftimport UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } @IBAction func FromViewControllertoTabBarController(_ sender: Any) { let storybord=UIStoryboard(name: "Main", bundle: nil) let TabBarController=storybord.instantiateViewController(withIdentifier: "TabBarController") as! UITabBarController //[0]が緑のViewControllerで[1]にすると赤のViewControllerに遷移する let ViewController=TabBarController.viewControllers?[0] as! FirstViewController //TabControllerに遷移した時に最初にどの画面を表示するかを選択する TabBarController.selectedViewController=ViewController TabBarController.modalTransitionStyle = .crossDissolve TabBarController.modalPresentationStyle = .fullScreen self.present(TabBarController, animated: true, completion: nil) } }







