- 投稿日:2020-08-01T23:48:33+09:00
Ionic AdMobのプラグインをcapacitor-communityのに乗り換えた話【iOSバージョン】
はじめに
元々はcapacitor-admobを使っていたのだが、このプラグインではどうやらCapacitor2にアップグレードした場合、Androidでビルドできない問題が起きた!(2020/8月現在)
なのでAdMobのプラグインを変更することにした。
iOSバージョンは比較的簡単に乗り換えれた。
と言う訳で、この記事はiOSバージョンでcapacitor-communityのAdMobプラグインに乗り換えた話。iOSの方は簡単だった理由
理由はズバリ、Android X!
(詳しくはこの記事のAndroidバージョンで紹介したいが、、、)Capacitor 2.0以降はAndroidXを使うようになった。「AndroidXって何やねん?」って感じだが、、まぁ詳しくは次の記事で書くとしよう。
前提
・Angularを使用
・Capacitorのバージョンは2.X(2以上ってこと)
→ Capacitorのアップグレード方法はこちら・capacitor-admobを使っていた(capacitor-communityのAdMobプラグインとほぼ同じで、コードを編集する必要はほぼ無かった。)
もしAbMobプラグインの使用手順を1から知りたい人は、こちらを参考に
ionic capacitorのAdMobプラグインを使う(capacitor-admob)インストール
まずはプラグインを入れる
npm install --save @capacitor-community/admob
そして以前まで使っていたプラグインを消す
npm uninstall capacitor-admob
プラグインの初期設定
ほぼuninstallしたcapacitor-admobと同じ
AppDelegate.swift
ios/App/App/AppDelegate.swift
を開く// 追加 import GoogleMobileAds @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? // 多分一番上にある func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // 追加 GADMobileAds.sharedInstance().start(completionHandler: nil)Info.plist
ios/App/App/Info.plist
を開く<key>GADIsAdManagerApp</key> <true/> <key>GADApplicationIdentifier</key> <string>[APP_ID]</string>「APP_IDって何やねん?」と言う方はこちらを参考に
ionic capacitorのAdMobプラグインを使う(capacitor-admob)app.component.tsで初期化
import { Plugins } from '@capacitor/core'; const { AdMob } = Plugins; @Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrls: ['app.component.scss'] }) export class AppComponent { constructor(){ if (platform.is('ios')) { AdMob.initialize('YOUR APPID for ios'); } else if (platform.is('android')) { AdMob.initialize('YOUR APPID for android'); } } }バナー広告を出してみる
AdMob用の
service
を作っておく。(もう作ってある人は不要)ionic g service service/admob
admob.service.tsimport { Injectable } from '@angular/core'; import { Platform } from '@ionic/angular'; import { Plugins } from '@capacitor/core'; import { AdOptions, AdSize, AdPosition } from '@capacitor-community/admob'; const { AdMob } = Plugins; @Injectable({ providedIn: 'root', }) export class AdmobService { constructor(private platform: Platform) {} iosOptions: AdOptions = { adId: 'ca-app-pub-XXXX', adSize: AdSize.SMART_BANNER, position: AdPosition.BOTTOM_CENTER, margin: 0, }; androidOptions: AdOptions = { adId: 'ca-app-pub-XXXX', adSize: AdSize.SMART_BANNER, position: AdPosition.BOTTOM_CENTER, margin: 0, }; showBanner() { if (this.platform.is('ios')) { // Show Banner Ad AdMob.showBanner(this.iosOptions).then( (value) => { console.log('iOS Banner: ' + value); // true }, (error) => { console.error(error); // show error } ); } else if (this.platform.is('android')) { AdMob.showBanner(this.androidOptions).then( (value) => { console.log('Android Banner: ' + value); // true }, (error) => { console.error(error); // show error } ); } // Subscibe Banner Event Listener AdMob.addListener('onAdLoaded', (info: boolean) => { console.log('Showing Banner AD.'); }); } hideBanner() { AdMob.hideBanner().then( () => { console.log('バナーhide'); }, (error) => { console.log(error); } ); } resumeBanner() { AdMob.resumeBanner().then( () => { console.log('バナーresume'); }, (error) => { console.log(error); } ); } }home.page.tsimport { AdmobService } from '../service/admob.service'; constructor(private admobService: AdmobService) {} ionViewWillEnter() { this.admobService.showBanner(); }変わった点
hasTabBar
とtabBarHeight
がなくなり、margin
になったぐらいかな?options: AdOptions = { adId: "Your AD_Id", adSize: AdSize.SMART_BANNER, position: AdPosition.BOTTOM_CENTER, hasTabBar: false, // make it true if you have TabBar Layout. tabBarHeight: 56 // you can assign custom margin in pixel default is 56 };capacitor-communityのAdMobプラグイン
options: AdOptions = { adId: "Your AD_Id", adSize: AdSize.SMART_BANNER, position: AdPosition.BOTTOM_CENTER, margin: 0 };AdMobのバナー広告のサイズについて
https://developers.google.com/android/reference/com/google/android/gms/ads/AdSize
このページを丸々翻訳にかけると、ある程度は理解できるかと
- 投稿日:2020-08-01T23:23:41+09:00
【Flutterの状態管理】テーマ切替&多言語化 〜provider,BLoC,reduxの3パターンで実現してみる②
お待たせしました。プロジェクトの人員異動でバタバタしていました。
さて、前回ではproviderという実現方式でflutterの状態管理を説明しました。今回はBLoC(Business Logic Component)を利用し、アプリのテーマ(Theme)切替、多言語化を通じてFlutterの状態管理を説明します。二、BLoCでテーマ切替&多言語化
Blocには3つのポイントがある。それは 業務ロジックユニット、状態、イベント。
flutter_bloc: ^6.0.1
1.テーマ切替のBloc
ハンバーガーメニューにあるテーマ色ボタンの押下により、グローバルにテーマ色を切替える。
1.1状態クラス
テーマ関連の状態量を抽象クラスに置きながら、他の状態はこれを継承し状態の細分化を実現する。
theme_state.dartimport 'package:flutter/material.dart'; @immutable abstract class BlocThemeState { final ThemeData themeData;//テーマ final int colorIndex;//色ボタン選択中の状態を記録 BlocThemeState(this.themeData, this.colorIndex); } class InitBlocThemeState extends BlocThemeState { //初期化テーマは緑にする InitBlocThemeState(ThemeData themeData, int colorIndex) : super(ThemeData(primaryColor: Colors.green), 1); } class BlocStateImpl extends BlocThemeState { BlocStateImpl(ThemeData themeData, int colorIndex) : super(themeData, colorIndex); }1.2イベントクラス
イベントをここで定義する。パラメーターの切り替え、状態のリセットなど。
theme_event.dartimport 'package:flutter/material.dart'; @immutable abstract class BlocThemeEvent {} class BlocEventSwitchTheme extends BlocThemeEvent{ final ThemeData themeData; final int colorIndex; BlocEventSwitchTheme(this.themeData,this.colorIndex); } class BlocEventResetTheme extends BlocThemeEvent{}1.3業務ロジッククラス
このクラスはBlocの核と言っても過言ではない。イベントを通じて状態を生成する。
theme_bloc.dartimport 'package:flutter_bloc/flutter_bloc.dart'; import 'package:state_dancer/BLoC/theme_event.dart'; import 'package:state_dancer/BLoC/theme_state.dart'; class ThemeBloc extends Bloc<BlocThemeEvent,BlocThemeState> { //overrideメソッド、状態の初期化 @override ThemeBloc(BlocThemeState initialState) : super(initialState); @override //async* yield 非同期処理関数を生成(あまり使わない) 一々setState()書かなくても済むのはメリット Stream<BlocThemeState> mapEventToState(BlocThemeEvent event) async* { //テーマ切替 if(event is BlocEventSwitchTheme) { yield BlocStateImpl(event.themeData, event.colorIndex); } //テーマリセット if(event is BlocEventResetTheme) { yield InitBlocThemeState(); } } }2.多言語化のBloc
ハンバーガーメニューにある言語ボタンの押下により、グローバルに言語を切替える。
2.1状態クラス
lang_state.dartimport 'package:flutter/material.dart'; @immutable abstract class BlocLocaleState { final Locale locale; BlocLocaleState(this.locale); } class JpBlocLocaleState extends BlocLocaleState { JpBlocLocaleState() : super(Locale('ja', 'JP')); } class InitBlocLocaleState extends JpBlocLocaleState {} class EnBlocLocaleState extends BlocLocaleState { EnBlocLocaleState() : super(Locale('en', 'US')); }2.2イベントクラス
lang_event.dartimport 'package:flutter/material.dart'; @immutable abstract class BlocLocaleEvent {} class BlocEventSwitch2JP extends BlocLocaleEvent{} class BlocEventSwitch2EN extends BlocLocaleEvent{}2.3業務ロジッククラス
lang_bloc.dartimport 'package:flutter_bloc/flutter_bloc.dart'; import 'package:state_dancer/BLoC/lang_event.dart'; import 'package:state_dancer/BLoC/lang_state.dart'; class LocalBloc extends Bloc<BlocLocaleEvent,BlocLocaleState> { //初期化コンストラクター @override LocalBloc(BlocLocaleState initialState) : super(initialState); @override Stream<BlocLocaleState> mapEventToState(BlocLocaleEvent event) async* { if(event is BlocEventSwitch2JP) { yield JpBlocLocaleState(); } if(event is BlocEventSwitch2EN) { yield EnBlocLocaleState(); } } }3.Blocの使用
providerパターンと同様に、管理する必要のある状態をMultiBlocProviderの中に入れる。
main.dartimport 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart';//providerパターンのpubspec.yaml配置を参照 import 'package:state_dancer/BLoC/lang_state.dart'; import 'package:state_dancer/BLoC/theme_state.dart'; import 'home_page.dart'; import 'provider/I18nDelegate.dart';//providerパターン紹介時のファイルを使用 import 'BLoC/theme_bloc.dart'; import 'BLoC/lang_bloc.dart'; void main() { runApp(Wrapper(child: MyApp())); } class Wrapper extends StatelessWidget { final Widget child; Wrapper({this.child}); @override Widget build(BuildContext context) { return MultiBlocProvider(providers: [ BlocProvider<ThemeBloc>(create: (context) => ThemeBloc(InitBlocThemeState()),), BlocProvider<LocalBloc>(create: (context) => LocalBloc(InitBlocLocaleState()),), ], child: MyApp()); } } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder<ThemeBloc, BlocThemeState>( builder: (_, theme) => BlocBuilder<LocalBloc, BlocLocaleState>( builder: (_, local) => MaterialApp( title: "状態管理Demo", localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, I18nDelegate.delegate //言語デリゲート 前回providerパターンのファイルを利用 ], locale: local.locale, supportedLocales: [local.locale], debugShowCheckedModeBanner: false, home: HomePage(), ))); } }BlocBuilder<~Bloc, ~State>(builder: (_,~State) を通じて状態ゲット;
BlocProvider.of<~Bloc>(context).add(~Event())は状態変更のトリガーhome_page.dartimport 'package:flutter/material.dart'; import 'package:state_dancer/BLoC/lang_bloc.dart'; import 'BLoC/theme_bloc.dart'; import 'BLoC/theme_state.dart'; import 'provider/i18n.dart';//providerパターン紹介時のファイルを使用 import 'package:flutter_bloc/flutter_bloc.dart'; import 'BLoC/theme_event.dart'; import 'BLoC/lang_event.dart'; class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { var screenWidth = MediaQuery.of(context).size.width; var statusBarH = MediaQuery.of(context).padding.top; var naviBarH = kToolbarHeight; //BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) を通じて状態ゲット return BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) =>Scaffold( appBar: AppBar( backgroundColor: blocState.themeData.primaryColor, title: Text(I18N.of(context).title,style: TextStyle(color: Colors.white),), ), drawer: Drawer( child: BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) { return Column( children: <Widget>[ Container( width: screenWidth, height: statusBarH + naviBarH, color: blocState.themeData.primaryColor, ), SizedBox(height: 10.0), Row( children: <Widget>[ Padding(padding: EdgeInsets.all(10)), Wrap(children: <Widget>[ RaisedButton( color: Colors.green, onPressed: () { //イベントトリガー、重要 BlocProvider.of<ThemeBloc>(context).add(BlocEventSwitchTheme(ThemeData(primaryColor: Colors.green), 1)); }, child: Text(I18N.of(context).greenBtn,style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.red, onPressed: () { BlocProvider.of<ThemeBloc>(context).add(BlocEventSwitchTheme(ThemeData(primaryColor: Colors.red), 2)); }, child: Text(I18N.of(context).redBtn,style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.blue, onPressed: () { BlocProvider.of<ThemeBloc>(context).add(BlocEventSwitchTheme(ThemeData(primaryColor: Colors.blue), 3)); }, child: Text(I18N.of(context).blueBtn,style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), ],), ], ), Divider(), SizedBox(height: 10,), Row(children: <Widget>[ Padding(padding: EdgeInsets.all(10)), SizedBox(width: 15,), RaisedButton( color: blocState.themeData.primaryColor, onPressed: () { BlocProvider.of<LocalBloc>(context).add(BlocEventSwitch2JP()); }, child: Text("日本語",style: TextStyle(color: Colors.white),), ), SizedBox(width: 15.0,), RaisedButton( color: blocState.themeData.primaryColor, onPressed: () { BlocProvider.of<LocalBloc>(context).add(BlocEventSwitch2EN()); }, child: Text("English",style: TextStyle(color: Colors.white),), ), ],), ], ); }), ), body: ListView( children: <Widget>[ BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) { return Center( child: Column( children: <Widget>[ SizedBox(height: 10.0), Container( width: 200, height: 300, color: blocState.themeData.primaryColor, ), SizedBox(height: 10.0), Text(I18N.of(context).analects, style: TextStyle( color: blocState.themeData.primaryColor, fontSize: 18.0, fontWeight: FontWeight.bold), ), SizedBox(height: 30.0), FloatingActionButton(onPressed: (){ }, backgroundColor: blocState.themeData.primaryColor, child: Icon(Icons.check)), ], ), ); }) ], ), ), ); } }まとめ
本文はBLocでテーマや多言語の切替という多状態を説明しました。次回ではreduxパターンをご紹介しますので、引き続きお楽しみください。
- 投稿日:2020-08-01T21:31:16+09:00
WebViewの表示方法
#WebViewをサクッと表示!!
まず、ViewController.fileにWebKitをimportします。
ViewController.swiftimport UIKit import WebKit class ViewController: UIViewController { @IBOutlet weak var webView: WKWebView! override func viewDidAppear(_ animated: Bool){ } }次に、main.storyboardへ。
WebKit Viewを右上の「+」 ボタンから選択し、ViewControllerへ貼り付け。次にwebViewを表示させるコードを書きます。
ViewController.swiftimport UIKit import WebKit class ViewController: UIViewController { @IBOutlet weak var webView: WKWebView! var topPadding:CGFloat = 0 override func viewDidAppear(_ animated: Bool){ print("viewDidAppear") if #available(iOS 11.0, *) { // 'keyWindow' was deprecated in iOS 13.0: Should not be used for applications let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first topPadding = window!.safeAreaInsets.top } let webUrl = URL(string: "https://www.google.com/")! let myRequest = URLRequest(url: webUrl) webView.load(myRequest) // インスタンスをビューに追加 self.view.addSubview(webView) }これでオッケーです!
ただ、これでシュミレーターを立ち上げると・・・
この様に、エラーが出てしまいます。
どうやら原因はフレームワークが追加されていないからとのこと。
ということで、追加。
これで完成です!
webViewを表示するのは非常に簡単ですね!
ビューアーなどの記事サイトを表示させる時に活躍しそうですね。
webkitViewの使用方法でした!ありがとうございました。
- 投稿日:2020-08-01T21:09:29+09:00
Swiftの実機テストする際に出たエラーの解決法
はじめに
いつもはSimulatorを使ってテストをしているのですが、自分のiPhoneでも試してみたいと思い、実機テストをやってみました。思っていたより梃子摺ったので遭遇したエラーの解決方法をまとめてみました。
「Signing for "アプリ名" requires a development team.」というエラーが出た場合
Signing & CapabilitiesでTeamを設定する。Apple IDでログインするとTeamという項目で自分のアカウントを選択できるようになるのでそこから設定する。
「Could not locate device support files.」というエラーが出た場合
https://github.com/filsv/iPhoneOSDeviceSupport
こちらのサイトから自分のiPhoneのiOSにあったファイルをダウンロードする。
Finderでcommand + shift + gを押すと、パスを指定して移動できるので、下のパスをコピペする。/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/開いたフォルダに先程Githubからダウンロードしたフォルダを移動させて、Xcodeを再起動する。
(もし13.5.1のようにバージョンが細かい場合は、Githubからダウンロードした13.5のファイル名を13.5.1のように変更すれば良い。)「Untrusted Developer」というエラーが出た場合
iPhoneで設定→General→Profile & Device Managementに移動すると、Developer Appという項目があるので、そこにある自分のアプリ名をクリックし、「<アプリ名>を信頼する」で「信頼」というボタンを押せば、自分のiPhoneで自作したアプリにアクセスできるようになります。
参考にしたサイト
https://qiita.com/solabito331/items/17ee51d954fb3f87b37c
https://cpoint-lab.co.jp/article/201906/10350/
https://qiita.com/segur/items/bef54efa7764885173bb
- 投稿日:2020-08-01T16:03:18+09:00
RiverpodとFlutter Hooksを使って、カウンターアプリを作ってみよう
はじめに
この記事では、RiverpodとFlutter HooksとStateNotifierを使って、
カウンターアプリを作ってみようと思います。今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_exampleFlutterの状態管理パターン
みなさん、Flutterの状態管理パターンは、何を使っていますでしょうか?
Flutterの状態管理パターンはいっぱいあります。
大すぎですw
- StatefulWidget
- Redux
- BLoC(Stream/InheritedWidget) + Provider
- ChangeNotifier + Provider
- StateNotifier + freezed + Provider
- StateNotifier + Flutter Hooks + Riverpod 【new】
僕は、これまでは「StateNotifier + freezed + Provider」を主に使っていました。
「StateNotifier + freezed + Provider」の使い方については、以下に記事を書きました。
https://qiita.com/karamage/items/4b1aff984b1af7541b73新たなパターン登場
そんな今日この頃ですが、状態管理パターンにキラボシのごとく輝く新星が現れました。
「RiverpodとFlutter Hooks」です。
最近、Flutter界隈では、RiverpodとFlutter Hooksが話題です。
@mono0926 さんがツイッターで、Riverpodの発言をしているのをよく拝見します。
最も取っ付きやすそうな例として、Riverpod(hooks非使用) + ChangeNotifierProviderのカウンターページ書いてみた( ´・‿・`)https://t.co/PyZlVky4zo pic.twitter.com/PS8gYI8hrt
— mono (@_mono) July 4, 2020ProviderとRiverpodの扱いの感覚の違い、いつか記事書くかもしれないけど、とりあえずライトにツイート( ´・‿・`)
— mono (@_mono) July 3, 2020
(特に2枚目が大事) pic.twitter.com/7ucPUdhdc6そこで、私もいっちょやってみようかなと思いました。
ざざっと、RiverpodとFlutterHooksを使ってカウンターアプリを作ってみましたので、当記事にまとめます。
Riverpodとは
Providerの作者による、Providerの進化版です。
Provider, but different
Riverpod公式ページ
https://riverpod.dev
「Providerってなに?」って方は、以下のページが詳しいです。
FlutterのProviderパッケージを使いこなす
https://itome.team/blog/2019/12/flutter-advent-calendar-day7/もともと使いやすかった「Provider」をさらに強化したのが「Riverpod」なのですね!
Riverpodの良いところ
Flutterに依存しなくなった(Pure Dart)
- ProviderがInheritedWidgetをラップしたものだった。
- RiverpodはInheritedWidgetを使わずゼロから構築した。
- Providerから状態を読み込む際、BuildContextが必要なくなった。
コンパイル時にエラーを検知できる
- Providerでは実行時にしかエラーが検知できなかった。
- よくあったのが、Providerから状態を読み出そうとして、「Providerが見つからない」エラー
同じ型のProviderを複数配信できる
ProviderをPrivateにできる
Widget Treeだけでなく、Model層でも利用できる
グローバル変数としてProviderを宣言できるようになった
使われていないProviderの状態をdisposeできる
Computedで、Providerの状態を使って計算した値を状態として持てる。
他にも、Family、AutoDisposeProviderなどの新機能多数
Hooks的な使い方も可能(ゆえにHooks APIとの相性がよい)
- Hooksなしでも使える
※ただし、Flutter公式はHooksを肯定的には捉えていない。今後の動向に注目。
Riverpodの悪いところ
- まだまだ開発段階。
- Hooks推しがどうなるか不明。(Flutter公式は否定的)
- これから破壊的な変更が入りそう
- プロダクトに組み込むにはまだ早い感じ
- stableされるまで待とう。(将来的には、Flutter標準に取り込まれるのを願う)
Flutter Hooks とは
https://github.com/rrousselGit/flutter_hooks
「React Hooks」のFlutter版みたいなイメージです。
参考:5分でわかるReact Hooks
https://qiita.com/Mitsuzara/items/98d1bc4a83265a764084https://riverpod.dev
Riverpodの公式ページを見る限り、RiverpodとFlutter Hooksはセットで使うのが、作者の推しみたいです。
RiverpodはFlutter Hooksを外しても利用できます。カウンターアプリを作ってみよう
Riverpod と Flutter Hooks と StateNotifierインストール
pubspec.yamlenvironment: sdk: ">=2.7.0 <3.0.0" flutter: ">= 1.17.0" dependencies: flutter: sdk: flutter flutter_hooks: ^0.11.0 hooks_riverpod: ^0.5.1 state_notifier: ^0.5.0pubspec.yamlに上記のように書いて、ターミナルで「flutter pub get」しましょう。
flutter pub getこれでインストールは完了です。
それでは、コードを書いていきましょう。
lib/main.dartimport 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:state_notifier/state_notifier.dart'; final counterProvider = StateNotifierProvider((_) => Counter()); class Counter extends StateNotifier<int> { Counter() : super(0); void increment() => state++; } void main() { runApp( ProviderScope( child: CounterApp(), ), ); } // Note: CounterApp is a HookWidget, from flutter_hooks. class CounterApp extends HookWidget { @override Widget build(BuildContext context) { final state = useProvider(counterProvider.state); final counter = useProvider(counterProvider); return MaterialApp( home: Scaffold( appBar: AppBar(title: Text('CounterApp')), body: Center( child: Text(state.toString()), ), floatingActionButton: FloatingActionButton( onPressed:() => counter.increment(), child: Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ), ); } }めちゃくちゃコードがすっきり書けて感動。
実行して、「+」を押すと数字がカウントアップすると思います。
コードの説明
グローバルにProviderを宣言することができる
final counterProvider = StateNotifierProvider((_) => Counter());ProviderだとWidget Treeのrootのほうで、Providerを作成していたと思います。
Riverpodでは、グローバルに、Widget Treeに縛られることなく、Providerの作成が可能です。Providerにスコープを設定できる
ProviderScope( child: CounterApp(), ),ProviderScopeを使えば、Providerにアクセスできる階層をコントロールすることができます。
Hooksをつかうためには、HookWidgetを継承する
class CounterApp extends HookWidget {将来的にはclassではなく、「React Hooks」と同じように関数で書けるようになるといいですね。
そのためにもFlutte公式がHooksを認めてもらえるといいなと思います。状態を読み込むには、useProviderを使う
final state = useProvider(counterProvider.state);めっちゃ簡単でシンプルですね。
BuildContextが必要なくなったのが良い!まとめ
- Riverpodめっちゃ良い。Providerから正当進化している。使わない理由がない。
- 今後は、Providerではなく、Riverpodを使っていきたい。
- しかしながら、Hooksがどうなるかわからないので、もうしばらくは様子見。
最後までお読みいただき、ありがとうございました。
今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_example参考リンク
Riverpod
https://riverpod.devFlutter Hooks
https://github.com/rrousselGit/flutter_hooksFlutter Hooks, say goodbye to StatefulWidget and reduce boilerplate code.
https://medium.com/flutter-community/flutter-hooks-say-goodbye-to-statefulwidget-and-reduce-boilerplate-code-8573d4720f9aexample counter
https://github.com/rrousselGit/river_pod/tree/master/examples/counterexample todo
https://github.com/rrousselGit/river_pod/tree/master/examples/todos
- 投稿日:2020-08-01T16:03:18+09:00
RiverpodとFlutter Hooksを使う、はじめの一歩
はじめに
この記事では、RiverpodとFlutter Hooksを使う、はじめの一歩として、
カウンターアプリを作ってみようと思います。今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_exampleFlutterの状態管理パターン
みなさん、Flutterの状態管理パターンは、何を使っていますでしょうか?
Flutterの状態管理パターンはいっぱいあります。
大すぎですw
- StatefulWidget
- Redux
- BLoC(Stream/InheritedWidget) + Provider
- ChangeNotifier + Provider
- StateNotifier + freezed + Provider
- StateNotifier + Flutter Hooks + Riverpod 【new】
私は、これまでは「StateNotifier + freezed + Provider」を主に使っていました。
「StateNotifier + freezed + Provider」の使い方については、以下に記事を書きました。
https://qiita.com/karamage/items/4b1aff984b1af7541b73新たなパターン登場
そんな今日この頃ですが、状態管理パターンにキラボシのごとく輝く新星が現れました。
「RiverpodとFlutter Hooks」です。
最近、Flutter界隈では、RiverpodとFlutter Hooksが話題です。
@mono0926 さんがツイッターで、Riverpodの発言をしているのをよく拝見します。
最も取っ付きやすそうな例として、Riverpod(hooks非使用) + ChangeNotifierProviderのカウンターページ書いてみた( ´・‿・`)https://t.co/PyZlVky4zo pic.twitter.com/PS8gYI8hrt
— mono (@_mono) July 4, 2020ProviderとRiverpodの扱いの感覚の違い、いつか記事書くかもしれないけど、とりあえずライトにツイート( ´・‿・`)
— mono (@_mono) July 3, 2020
(特に2枚目が大事) pic.twitter.com/7ucPUdhdc6そこで、私もいっちょやってみようかなと思いました。
ざざっと、RiverpodとFlutterHooksを使ってカウンターアプリを作ってみましたので、当記事にまとめます。
Riverpodとは
Providerの作者による、Providerの進化版です。
Provider, but different
Riverpod公式ページ
https://riverpod.dev
「Providerってなに?」って方は、以下のページが詳しいです。
FlutterのProviderパッケージを使いこなす
https://itome.team/blog/2019/12/flutter-advent-calendar-day7/もともと使いやすかった「Provider」をさらに強化したのが「Riverpod」なのですね!
Riverpodの良いところ
Flutterに依存しなくなった(Pure Dart)
- ProviderがInheritedWidgetをラップしたものだった。
- RiverpodはInheritedWidgetを使わずゼロから構築した。
- Providerから状態を読み込む際、BuildContextが必要なくなった。
コンパイル時にエラーを検知できる
- Providerでは実行時にしかエラーが検知できなかった。
- よくあったのが、Providerから状態を読み出そうとして、「Providerが見つからない」エラー
同じ型のProviderを複数配信できる
ProviderをPrivateにできる
ProviderをWidget Tree限定でなく、Model/Domain Logic/Repositoryどこでも利用できる
グローバル変数としてProviderを宣言できるようになった
使われていないProviderの状態をdisposeできる
Computedで、Providerの状態を使って計算した値を状態として持てる。
他にも、Family、AutoDisposeProviderなどの新機能多数
Hooks的な使い方も可能(ゆえにHooks APIとの相性がよい)
- Hooksなしでも使える
※ただし、Flutter公式はHooksを肯定的には捉えていない。今後の動向に注目。
Riverpodの悪いところ
- まだまだ開発段階。
- Hooks推しがどうなるか不明。(Flutter公式は否定的)
- これから破壊的な変更が入りそう
- プロダクトに組み込むにはまだ早い感じ
- stableされるまで待とう。(将来的には、Flutter標準に取り込まれるのを願う)
Flutter Hooks とは
https://github.com/rrousselGit/flutter_hooks
「React Hooks」のFlutter版みたいなイメージです。
参考:5分でわかるReact Hooks
https://qiita.com/Mitsuzara/items/98d1bc4a83265a764084https://riverpod.dev
Riverpodの公式ページを見る限り、RiverpodとFlutter Hooksはセットで使うのが、作者の推しみたいです。
RiverpodはFlutter Hooksを外しても利用できます。カウンターアプリを作ってみよう
Riverpod と Flutter Hooks と StateNotifierインストール
pubspec.yamlenvironment: sdk: ">=2.7.0 <3.0.0" flutter: ">= 1.17.0" dependencies: flutter: sdk: flutter flutter_hooks: ^0.11.0 hooks_riverpod: ^0.5.1 state_notifier: ^0.5.0pubspec.yamlに上記のように書いて、ターミナルで「flutter pub get」しましょう。
flutter pub getこれでインストールは完了です。
それでは、コードを書いていきましょう。
lib/main.dartimport 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:state_notifier/state_notifier.dart'; final counterProvider = StateNotifierProvider((_) => Counter()); class Counter extends StateNotifier<int> { Counter() : super(0); void increment() => state++; } void main() { runApp( ProviderScope( child: CounterApp(), ), ); } // Note: CounterApp is a HookWidget, from flutter_hooks. class CounterApp extends HookWidget { @override Widget build(BuildContext context) { final state = useProvider(counterProvider.state); final counter = useProvider(counterProvider); return MaterialApp( home: Scaffold( appBar: AppBar(title: Text('CounterApp')), body: Center( child: Text(state.toString()), ), floatingActionButton: FloatingActionButton( onPressed:() => counter.increment(), child: Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ), ); } }めちゃくちゃコードがすっきり書けて感動。
実行して、「+」を押すと数字がカウントアップすると思います。
コードの説明
グローバルにProviderを宣言することができる
final counterProvider = StateNotifierProvider((_) => Counter());ProviderだとWidget Treeのrootのほうで、Providerを作成していたと思います。
Riverpodでは、グローバルに、Widget Treeに縛られることなく、Providerの作成が可能です。Providerにスコープを設定できる
ProviderScope( child: CounterApp(), ),ProviderScopeを使えば、Providerにアクセスできる階層をコントロールすることができます。
Hooksをつかうためには、HookWidgetを継承する
class CounterApp extends HookWidget {将来的にはclassではなく、「React Hooks」と同じように関数で書けるようになるといいですね。
そのためにもFlutte公式がHooksを認めてもらえるといいなと思います。状態を読み込むには、useProviderを使う
final state = useProvider(counterProvider.state);めっちゃ簡単でシンプルですね。
BuildContextが必要なくなったのが良い!まとめ
- Riverpodめっちゃ良い。Providerから正当進化している。使わない理由がない。
- 今後は、Providerではなく、Riverpodを使っていきたい。
- しかしながら、Hooksがどうなるかわからないので、もうしばらくは様子見かなぁ。
- Hooksを外して使うのが良いかも。
- まずは個人アプリで採用してみようと思います。
最後までお読みいただき、ありがとうございました。
今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_example参考リンク
Riverpod
https://riverpod.devFlutter Hooks
https://github.com/rrousselGit/flutter_hooksFlutter Hooks, say goodbye to StatefulWidget and reduce boilerplate code.
https://medium.com/flutter-community/flutter-hooks-say-goodbye-to-statefulwidget-and-reduce-boilerplate-code-8573d4720f9aexample counter
https://github.com/rrousselGit/river_pod/tree/master/examples/counterexample todo
https://github.com/rrousselGit/river_pod/tree/master/examples/todos
- 投稿日:2020-08-01T14:01:45+09:00
selfって書いたり書かなかったりすることない???
以前働いていた会社ではソースコードで絶対にselfをつけていたのに、ウェブで読むソースコードではほとんどselfがついてないことが多い。たまにエラーが出るからつけときましょう、っていうのが多い。
なんで?つけるのつけないのどっち???
Swiftはselfを書かなくてもメンバ変数にアクセスが可能です。
ただ、ローカル変数と衝突した際にはselfを付ける必要があります。なるほど〜〜〜。
上記サイトにもメリットデメリット載っており、意見を書かれているので、ここでの結論としては、
Xcodeに怒られたらつけとこ〜〜〜?⭐️?⭐️?⭐️
- 投稿日:2020-08-01T13:47:44+09:00
clipToBoundsとは
アイコン画像を丸くするときなどに、
self.iconImageView.layer.cornerRadius = self.iconImageView.frame.size.width * 0.5 self.iconImageView.clipsToBounds = trueと書いた時の疑問。
cornerRadiusは角丸にするためのコードだと理解できるけど、その下のclipsToBoundsってなんでいつもtrueにせんとあかんのや、と。調べて出てきたのは、clipsToBoundsの特徴をまとめた下記URL。
https://qiita.com/orimomo/items/7075fb911da5c9d2fba5UIView(+それを継承しているUIImageView等)のプロパティである
Viewにセットしたコンテンツが、領域boundsの外を描画するかどうかを決定する
デフォルト値がfalseで、trueにすると領域内に限定されるよくわからない・・・
と思い、下に読み進めていくと、わかりやすい画像が載っていた。
ここで記載するのは割愛しますが、わかりやすくいうと、imageViewを配置して画像をセットしたとき、
false: 画像がはみ出した状態で出力
true: 画像をimageView内に収める
って感じです。つまり、画像の角の数値を決めたりとかするためにclipsToBoundsをtrueに定義しているわけではなく、丸いimageViewを決めた後に、アイコンをセットした際、その枠内に当て嵌めるようにしてるんですね。ナットクトクコさん。
- 投稿日:2020-08-01T13:34:49+09:00
全く需要が無さそうだがSwiftUIでヘブライ文字を覚えるiOSアプリを作った
はじめに
iOSアプリ作りたいという人の精神的な手助けになったらいいなと思って書きます。というのは、アプリを作るということの全体像を描くのに意味があるのではと考えた次第です。なので細かいことはそこそこにして、完成までに何をやったかを一通り書きたいと思います。
できたもの
ヘブライ文字の暗記クイズアプリ
https://github.com/tomita28/alephbeth
https://apps.apple.com/jp/app/%E3%83%98%E3%83%96%E3%83%A9%E3%82%A4%E6%96%87%E5%AD%97/id1512330663?l=en動機
- 最近プログラム書いてないなあ。なんか書きたいなあ。
- 日本語でヘブライ語勉強するアプリ誰も作ってへんやないか。
- SwiftUIなら簡単にアプリが作れるらしい!(本当に?)
- ところで知り合いがヘブライ語勉強したいらしいがヘブライ文字暗記アプリがいいのないな。
- ないものは作るしかない♪ https://www.youtube.com/watch?v=6zr6pWwHACU
リリースまでにやったこと
- SwiftUIのチュートリアルをやった。
- ヘブライ文字のラテンアルファベット転写について調べた。
- 作りながら仕様を考えた。
- XCodeでコードを書いた。(ここまでで三日くらい(大変))
- kritaでiconを描いた。(これに一日(大変))
- プライバシーポリシーの設置
- Googleのバナーを貼った
- DeployGateでテスト及びリリース作業をした。(これに一日(大変))
- いろいろ変更点が出てきてもう何がなんだか。(あれから何ヶ月たったろう)
詳しい話
SwiftUIのチュートリアル
よくチュートリアルを終えると完全に理解した気になるっていうけど、私みたいな初級者にとってはチュートリアルを最後までやり切る勤勉さというのが本当に必要なんだなって思いました。新しい概念やそもそも知らない概念が出てくるときに、一番理解できる可能性が高いのは結局チュートリアルのなかであって、それらを解説してくれる誰かの説明は、あくまで自分でもチュートリアルをやったあとで効いてくるのだなというのが正直な感想でした。
とはいえ、自分のアプリに必要なものでいうと全部をやる必要もなかったので、SwiftUI TutoarlのEssentialsというところまでは最低やった方がいいと思いました。公式 https://developer.apple.com/tutorials/swiftui/creating-and-combining-views
大事だった概念
すいません、私には厳密な説明ができないのでざっくりした"気持ち"の部分だけ。
SwiftUIとView
従来のアプリの画面の作り方と異なり(やったことないけど)、ViewというObjectをコード上に書いていくことが、そのまま画面の配置になる。正直こういう仕組みでなければ自分は最後までこのアプリ作ろうと思えなかったと思う。画面のデザインをドラッグしていじったりというのはどうもうまく想像できない。
@State
Viewの中で随時変更できる値を保持している
@ObservedObject
Viewをまたいで保持可能だが、新たなインスタンスを作るごとに新しいデータが保持される。このアプリではネストしていくクイズの不正答のリストにあたる。
@EnvironmentObject
アプリ全体を通して保持して欲しいデータ。このアプリではユーザー設定の部分。
ヘブライ文字のラテンアルファベット転写方式
ヘブライ文字というのは広義のアルファベット一種であり(その源流と言ってもいい)、אבגדと言った文字のことです。これを覚えるためには例えばא(alef)と言ったように、英語にも使われているラテンアルファベットで表記するのが一番手取り早い方法です。しかし、ラテンアルファベット自体は世界共通の発音体系なわけではありません。また、ヘブライ語の全ての発音を網羅できているわけでもありません。そこで、いくつかの流儀によってヘブライ文字をラテンアルファベットに置き換える(転写する)方式を取らねばなりません。そこには恣意性があり、歴史性があります。最初のバージョンではイスラエルでよく見かける方式に統一していたのですが、某先生からある種の聖書(古典)ヘブライ語転写方式にも対応して欲しいと言われて、いろいろ教科書をひっぱり出しておりました。
転写方式
- Hebrew Academy
- Thomas O Lambdin
仕様を考える
プログラマをしていた時期が少しだけありましたが、ここまで自由に自分で仕様を考えることはありませんでした。結局仕様は作りながら考えるのが早かった。幸い、SwiftUIはPreviewsという概念のおかげで、Viewのコードを書いたそのすぐ下に、テストになるもの(具体的な値を代入した時のViewクラスの挙動を確認する仕組み)があるので、ほとんどテストコードを同時にかけるのでめちゃ便利でした。逆にこのPreviewsの機能を誤解していたせいで後述のNavigationViewを画面一つ一つに書かなければならないと思ってしまったところがあります。NavigationViewの子要素ではNavigationLinkの挙動を直接確認できないというのがPreviewsの仕様だと自分では納得しております。
で、結局仕様ですが、
- ヘブライ文字子音の呼称一覧
- 母音記号の呼称一覧
- 文字(子音、母音両方)クイズ
- 不正答の文字を一覧で再度表示する
- 不正答の文字だけをクイズする
- 再度不正答の文字について何層でもクイズ、一覧表示できるようにする。
- ラテンアルファベット転写方式を最初の画面で選択できるようにする。
となりました。
XCodeで頑張った
ここでは簡単にお世話になった概念を紹介するのみにします。
NavigationView
矢印をタップするとどんどん次の画面が開きます(iPhoneの設定画面みたいなやつ)
大事なことですがNavigationLinkを貼るのだけはたくさんやってよくて、NavigaitonViewを設置し直すと、目次のバーが重複します。このような使い方はあまりないと思いますので、通常は大元のViewの中に(例えばContentViewの直下に)NavigationViewを設置し、その下は全てNavigationLinkを貼るだけにしてください。MainView.swiftimport SwiftUI struct MainView: View { @EnvironmentObject var userData: UserData @State var showingSetting = false var body: some View { VStack{ NavigationView{ List{ HStack{ Spacer() Text("メニューを選んでね") .fontWeight(.thin) } NavigationLink(destination: ContentView( letters: lettersData[0].letters, pickers: lettersData[0].letters, title: "ヘブライ文字クイズ" )) ...ContentView.swiftimport SwiftUI struct ContentView: View { var ... var body: some View { VStack { ... Spacer() HStack{ NavigationLink( destination: ResultView( quizData: quizData, withUnderScores: self.withUnderScores, questionAmount: self.questionedLetters?.count, letters: letters, pickers: pickers, percent: self.completedRate() ) .environmentObject(userData), isActive: $goResultView) { Text("") } ...Alert
AlertからNavigationLinkを使う方法が面倒かった。
ContentView.swiftNavigationLink( destination: ResultView( quizData: quizData, withUnderScores: self.withUnderScores, questionAmount: self.questionedLetters?.count, letters: letters, pickers: pickers, percent: self.completedRate() ) .environmentObject(userData), isActive: $goResultView) { Text("") } Button("結果を見る"){ if(self.quizData.unQuestionedLetters.count > 0){ self.showingUnfinishedAlert = true } else { self.goResultView = true } }.alert(isPresented: $showingUnfinishedAlert) { Alert(title: Text("未終了"), message: Text("まだ全ての問題に回答していません。このクイズを終了して結果を見ますか?"), primaryButton: .destructive(Text("結果を見る")) { self.goResultView = true }, secondaryButton: .cancel()) }カスタムフォント
今回ヘブライ文字の筆記体を表示する必要があったので、フォントを使いしました。info.plistを編集する必要があるので注意してください。
既知の問題
筆記体のヘブライ文字はイタリックの文字のように右に傾いており、上下に幅の広い文字の場合、右上と左下に大きく伸びます。これが表示上左右の出っ張りが切れて見えなくなるという問題があります。ちなみに左右にスペースを入れても直りません。今回解決できなかったので、左右に_を入れて対処しました。
LetterExplanation.swiftScrollView{ Text("いいから覚えるんだ") .padding() if(withUnderScores ?? true){ Text("左右の横線(アンダースコア)を文字の高さの基準にしてください。") .padding() } self.displayFont(text: Text(self.textWithUnderScores(string: letter.script)), name: "活字体") if(!(letter.dagesh ?? true)){ self.displayFont(text: Text(self.textWithUnderScores(string: letter.script)) .font(.custom("KtavYadCLM-BoldItalic", size: 150)) , name:"筆記体") } ... func textWithUnderScores(string: String) -> String { if (self.withUnderScores ?? true ) { return "_" + string + "_" }else{ return string } }この_をとったらこの現象がおこると理解していただければ。いやあ、どうしたらいいんだろ。
SheetView
したから上にスッと出てくる画面ですね。設定画面で使いました。意外と面倒。
ContentView.swiftButton(action: { self.showingSetting.toggle() }) { Image(systemName: "gear").imageScale(.large) }.sheet(isPresented: $showingSetting) { SettingView( showSheetView: self.$showingSetting// userData: self.userData ) .environmentObject(self.userData) }SettingView.swiftimport SwiftUI struct SettingView: View { @Binding var showSheetView: Bool @EnvironmentObject var userData: UserData var body: some View { NavigationView { Form { Picker( selection: $userData.transliterationMode, label: Text("ラテン文字転写の方法")) { ForEach( TransliterationMode .allCases .filter{switch $0 { case TransliterationMode.Common: return false default: return true }} , id: \.self) { Text($0.rawValue).tag($0) } } } .navigationBarTitle(Text("設定"), displayMode: .inline) .navigationBarItems(trailing: Button(action: { print("Dismissing sheet view...") self.showSheetView = false }) { Text ("完了") } ) } } }kritaでアイコンを作ろう!
自分一人でアプリ作ろうとしたらアイコンも自作か...まじめんどくさいな。いろいろソフトあるみたいですがkritaというのをインストールしました。大したことはしてないのですが。この辺は気合ですね。デザインに死ぬほど自信がないので他人に見てもらったりしました。
プライバシーポリシーの設置
最近は個人の開発者でもプライバシーポリシーを設置することを求められるそうです。というかリリース作業の途中にめっちゃAppleに念押しされました。というわけでGitHub上にサイトを設置し、そこへのリンクを貼っておきました。今のところこれでBanとかはないです(2020/7)。
バナーをはろう!
正直全く儲かる気がしないのだけど、それでも練習と思ってGADBannerというのを貼りました。
DeployGateは全てを解決する!
他人にテストしてもらったり、そもそもリリースとかバージョンアップとか、DeployGateめっちゃ便利。
そもそもどうやったらApple Storeにアップできるのか全然わからない状況においてDeployGateのマニュアルは非常に優秀だった。リリース作業
実はここで一番ハマったのは「アプリ紹介用のスクショを貼る」という作業。simulatorでスクショしても全然サイズ合わへんヤンけ!
そもそもsimulatorはpixelを合わせて画面上に表示される(よくわかってない)。一応windowのタブからリアルなサイズに合わせることもできるようだが、それでも必要なサイズにはならない。結局無理やりリサイズした。なんかスマートな方法はないものか。その後
某先生からの指摘によりユーザー設定で転写方式を変えるためにいろいろしました。そのへんを含めてXCodeのところには書いておきました。
感想
実際にやってみるっていうのは一番楽しいことであって、採算とか目的とかはある程度度外視して、このアプリ作ってみて本当に楽しかった。出来上がったらいつもお世話になってるqiitaになんか書こうと思ってたのですが、いろいろ引っ張ってきてたリンクをもうたどることができない...。みんなどうやって記事書いてるんだ。作りながら書いてる? 今度からは記事を書きながら作業するのが目標になりそう。
- 投稿日:2020-08-01T12:55:39+09:00
仮置きでゴリ押しする、イラストロジック解析アプリ
はじめに
iOS / Android で動作する解析アプリを作りました。
邪道なことは重々承知していますが、イラストロジック雑誌の難問が解けないのは悔しいので。プロジェクト
プロジェクトはこちら。
仮置き
このアプリは解析に行き詰まると、まだ埋まっていないマスに仮置きして解析を続行します。
例えば、あるマスを「黒」と仮定して解析をすすめた結果、ロジックに矛盾が生じたとします。
ロジックが成り立たないということは「起点となったマスは黒であってはならない」ということがわかります。
言い換えると「起点となったマスは白である」となるわけで、仮置きした「黒」を「白」に反転確定させて解析を進めます。仮置き中に行き詰まると、さらに仮置きをします。
もし、延々と仮置きをしていって全てのマスが矛盾なく埋まった場合は、仮置きが全て正しかったとしてロジッククリアとします。↓ 仮置きでゴリ押しする様子(仮置きを起点としたマスはムラサキ色で表示)
思ったこと
サンプル用にイラストロジックの問題をいくつか作ったのですが難しかったです。
仮置きばかり発生して、ロジック的な面白さの皆無な問題ばかり出来上がりました。ほとんど仮置きせずに解き進められる、雑誌などの絶妙なバランスはすごいと痛感しました。
- 投稿日:2020-08-01T11:23:26+09:00
XcodeでArchiveしようとしたらbuildはsuccessだがorganizerに現れない
iPhoneアプリの更新リリースしようと思ったらなぜか出鼻を挫かれた。
環境
Xcode Version 11.6
MacOS 10.15.4現象
Product->Archiveするといつものようにbuildの時間に入る。
"Build Succeeded"の表示が出て、通常ならこのままOrganizerが立ち上がり、Archiveの履歴が表示されるはずである。
しかし表示されない。
Window->Organizerで開いて見ても今作ったものが履歴に表示されない。これいかに。原因
https://developer.apple.com/forums/thread/127212
↑と同じことが起きているようである。
/Users/****/Library/Developer/Xcode/Archivesまできて
この下には日付ごとのディレクトリがある。
しかし
2020-08-01だけが所有者がrootになっておりdrwxr-xr-xときた。
書き込めてないんじゃない?解決策
その場しのぎではあるが、2020-08-01の権限を777にするとarchiveはうまくいった。
もしかしてだけど
明日になったらまた同じことおこる?
それは明日考えよう。
- 投稿日:2020-08-01T02:52:55+09:00
【Swift】ゼロからのCombineフレームワーク - とりあえずViewにデータバインディングしたいための人の最小実装
意外と難しかったCombine
RxSwiftならある程度使えるし、簡単に使えるようになるかな?と思っていました。
しかし、いざ試してみるとなかなか苦労したので、まずは普段の開発に取り入れられる部分から切り出してみました。Viewに値をバインドする最小実装 UIKit版
import Combine import UIKit class ViewModel { @Published var labelText: String = "Default value." } class ViewController: UIViewController { var label: UILabel! // 適当な場所(storyboard, viewDidLoadなど)で初期化する let viewModel = ViewModel() private var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() viewModel.$labelText .map({ Optional($0) }) .receive(on: DispatchQueue.main) .assign(to: \UILabel.text, on: label) .store(in: &cancellables) } }適当なタイミングで、
viewModel.labelText = "Hello!"
などと値を変更すると、自動的にUILabel
が更新されます。解説
@Published
とPublisher
プロトコルバインドしたいプロパティの変更を監視するために、
import Combine
したうえで、@Published
というProperty Wrapperをバインドしたいプロパティに付与します。ViewModelclass ViewModel { @Published var labelText: String = "Default value." }Property Wrapperを付与したプロパティをもつクラスには、自動的に
$labelText
のようにプロパティ名の先頭に$
がついたgetterが生成されます。@Published
を付与した場合、このgetterはPublished<Output>.Publisher
型になります。
Publisher
というのが監視される側のプロトコルです(RxSwiftならObservable
)。また、この場合Output = String
です。バインド
Publisher
に準拠したプロパティをUIView
にバインドするには、assign
メソッドを使います。viewModel.$labelText .map({ Optional($0) }) .receive(on: DispatchQueue.main) .assign(to: \UILabel.text, on: label) .store(in: &cancellables)
assign
assign
メソッドの定義は次のようになっています。public func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root) -> AnyCancellable
keyPath
には\UILabel.text
1のようにバインド先のUIView
の型名とそのプロパティ名を記載します。
assign
メソッドはAnyCancellable
というCancellable
プロトコルに準拠したクラスのインスタンスを返します。このインスタンスのcancel
メソッドを呼ぶことでバインド(監視)が解除されます。また、
AnyCancellable
のインスタンスはメモリから解放されるタイミングでもcancel
メソッドを呼んでバインドを解除するようです2。そのため、ViewController
にSet<AnyCancellable>
型のプロパティを持たせて参照を保持します。追加する際にチェーンでメソッドを呼べるようにstore
メソッドが用意されています。
map
Publisher
には様々なオペレーターが用意されており、監視している値に変更を加えたり、流れてくる時間や回数などの条件によって流れをせきとめたりすることができます。
map
はもっとも基本的なオペレーターで、流れてきた値を加工することができます。
ここでは、UILabel.text: String?
の型に合わせて、ViewModel.labelText: String
をOptional
型に変換しています。
receive(on: DispatchQueue.main)
receive(on:)
はメソッドチェーンのうちで、このメソッド以降のチェーン内の処理が行われるスレッドを指定することができます。
assign
メソッドはUIの更新処理にあたるので、メインスレッドを指定しています。Viewに値をバインドする最小実装 SwiftUI版
import Combine import SwiftUI class ViewModel: ObservableObject { @Published var labelText: String = "" } struct ContentView: View { @ObservedObject private var viewModel = ViewModel() var body: some View { NavigationView { VStack { Text(viewModel.labelText) TextField("Title", text: $viewModel.labelText) .multilineTextAlignment(.center) } } } }解説
@ObservedObject
/ObservableObject
SwiftUIのViewにバインドするには、バインドしたいプロパティをもつ
viewModel
に@ObservedObject
を付与します。
(ObservableObject
プロトコルに準拠させることで、@ObservedObject
が付与できるようになります。)struct ContentView: View { @ObservedObject private var viewModel = ViewModel() ... } class ViewModel: ObservableObject { @Published var labelText: String = "" }バインド
var body: some View { NavigationView { VStack { Text(viewModel.labelText) TextField("Title", text: $viewModel.labelText) .multilineTextAlignment(.center) } } }
@ObservedObject
を付与したプロパティはView
に監視されるようになり、値の変更があればUIが自動更新されます。
そのため、通常のString
型を引数にとるクラスではviewModel.labelText
を渡すだけでバインド完了です。
@ObservedObject
によって生成された$viewModel
プロパティのlabelText
はBinding<String>
という型を返します。これをBinding<String>
型を引数にとるTextFieldに渡すことによって、双方向バインディングとなります。こちらのSwiftUI版では、
viewModel.labelText
の値を変更するとText
とTextField
の値が更新されますし、キーボードからTextField
を変更しても、viewModel.labelText
(とText
)の値が更新されます。
バックスラッシュはKey-Pathを表現するときの記法です。ドキュメントはこちら Key-Path Expression ↩
ドキュメントに記述を見つけられなかったのですが、参照を保持しなかった場合は
viewDidLoad
を抜けた時点でcancel
が呼ばれていました。 ↩
- 投稿日:2020-08-01T02:52:55+09:00
【Swift】ゼロからのCombineフレームワーク - UIKitでデータバインディング
意外と難しかったCombine
RxSwiftならある程度使えるし、簡単に使えるようになるかな?と思っていました。
しかし、いざ試してみるとなかなか苦労したので、まずは普段の開発に取り入れられる部分から切り出してみました。UIKitに値をバインドする
import Combine import UIKit class ViewModel { @Published var labelText: String = "Default value." } class ViewController: UIViewController { var label: UILabel! // 適当な場所(storyboard, viewDidLoadなど)で初期化する let viewModel = ViewModel() private var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() viewModel.$labelText .map({ Optional($0) }) .receive(on: DispatchQueue.main) .assign(to: \UILabel.text, on: label) .store(in: &cancellables) } }適当なタイミングで、
viewModel.labelText = "Hello!"
などと値を変更すると、自動的にUILabel
が更新されます。解説
@Published
とPublisher
プロトコルバインドしたいプロパティの変更を監視するために、
import Combine
したうえで、@Published
というProperty Wrapperをバインドしたいプロパティに付与します。ViewModelclass ViewModel { @Published var labelText: String = "Default value." }Property Wrapperを付与したプロパティをもつクラスには、自動的に
$labelText
のようにプロパティ名の先頭に$
がついたgetterが生成されます。@Published
を付与した場合、このgetterはPublished<Output>.Publisher
型になります。
Publisher
というのが監視される側のプロトコルです(RxSwiftならObservable
)。また、この場合Output = String
です。バインド
Publisher
に準拠したプロパティをUIView
にバインドするには、assign
メソッドを使います。viewModel.$labelText .map({ Optional($0) }) .receive(on: DispatchQueue.main) .assign(to: \UILabel.text, on: label) .store(in: &cancellables)
assign
assign
メソッドの定義は次のようになっています。public func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root) -> AnyCancellable
keyPath
には\UILabel.text
1のようにバインド先のUIView
の型名とそのプロパティ名を記載します。
assign
メソッドはAnyCancellable
というCancellable
プロトコルに準拠したクラスのインスタンスを返します。このインスタンスのcancel
メソッドを呼ぶことでバインド(監視)が解除されます。また、
AnyCancellable
のインスタンスはメモリから解放されるタイミングでもcancel
メソッドを呼んでバインドを解除するようです2。そのため、ViewController
にSet<AnyCancellable>
型のプロパティを持たせて参照を保持します。追加する際にチェーンでメソッドを呼べるようにstore
メソッドが用意されています。
map
Publisher
には様々なオペレーターが用意されており、監視している値に変更を加えたり、流れてくる時間や回数などの条件によって流れをせきとめたりすることができます。
map
はもっとも基本的なオペレーターで、流れてきた値を加工することができます。
ここでは、UILabel.text: String?
の型に合わせて、ViewModel.labelText: String
をOptional
型に変換しています。
receive(on: DispatchQueue.main)
receive(on:)
はメソッドチェーンのうちで、このメソッド以降のチェーン内の処理が行われるスレッドを指定することができます。
assign
メソッドはUIの更新処理にあたるので、メインスレッドを指定しています。おまけ:SwiftUI版
import Combine import SwiftUI class ViewModel: ObservableObject { @Published var labelText: String = "" } struct ContentView: View { @ObservedObject private var viewModel = ViewModel() var body: some View { NavigationView { VStack { Text(viewModel.labelText) TextField("Title", text: $viewModel.labelText) .multilineTextAlignment(.center) } } } }解説
@ObservedObject
/ObservableObject
SwiftUIのViewにバインドするには、バインドしたいプロパティをもつ
viewModel
に@ObservedObject
を付与します。
(ObservableObject
プロトコルに準拠させることで、@ObservedObject
が付与できるようになります。)struct ContentView: View { @ObservedObject private var viewModel = ViewModel() ... } class ViewModel: ObservableObject { @Published var labelText: String = "" }バインド
var body: some View { NavigationView { VStack { Text(viewModel.labelText) TextField("Title", text: $viewModel.labelText) .multilineTextAlignment(.center) } } }
@ObservedObject
を付与したプロパティはView
に監視されるようになり、値の変更があればUIが自動更新されます。
そのため、通常のString
型を引数にとるクラスではviewModel.labelText
を渡すだけでバインド完了です。
@ObservedObject
によって生成された$viewModel
プロパティのlabelText
はBinding<String>
という型を返します。これをBinding<String>
型を引数にとるTextFieldに渡すことによって、双方向バインディングとなります。こちらのSwiftUI版では、
viewModel.labelText
の値を変更するとText
とTextField
の値が更新されますし、キーボードからTextField
を変更しても、viewModel.labelText
(とText
)の値が更新されます。
バックスラッシュはKey-Pathを表現するときの記法です。ドキュメントはこちら Key-Path Expression ↩
ドキュメントに記述を見つけられなかったのですが、参照を保持しなかった場合は
viewDidLoad
を抜けた時点でcancel
が呼ばれていました。 ↩
- 投稿日:2020-08-01T00:59:13+09:00
Swiftでパズルゲーム制作を開始した
動機
今まで独自のゲームを作ってばかりでしたが、全然売れなかったので、有名ゲームをパク・・参考にすることにしました。
とりあえず形から入ろうということで、ドラゴンとパズルをテーマにすることにしました。このテーマで有名なゲームを参考にしていたのですが、「面白いか?」と単純な疑問を抱きました。
もう少し分かりやすいルールのパズルの方が良いのではと思い、別の有名ゲームを参考にしました。
ツムツムかポコパンかよく覚えていませんが、その辺りのゲームのルールが分かりやすそうだったので、パズル部分は隣接するアイテムをなぞって消すルールにしました。現状
TestFlightでベータ版をテストしている状態です。
パズルゲーム制作途中。
— 泉芳樹 #voxel (@izumi_yoshiki) July 31, 2020
ベータ版を試してもらっています。
TestFlightを使ってiPhoneでテストしてます。#ゲーム制作 #IndieGameDev #パズル pic.twitter.com/kxb8rFVemwゲームロジック
動きの処理は、1つの要素の下の要素と当たり判定をとって、当たってなければ落下する感じです。
消す処理は、同じ色の要素を選択したら消える。同じ番号の要素を選択したら消える。同じ形の要素を選択したら消える。という感じです。下記は同色の要素がなぞられたかを判断しています。
var i: Int = -1 var count1: Int = 0 for e in elements { if e.select { if i != -1 { if i == e.colorNumber { e.delete = true e.deleteY = e.shape?.position.y count1 += 1 } else { count1 = -1 break } } i = e.colorNumber! } } if count1 >= 1 { for e in elements { if e.select { if i == e.colorNumber{ e.delete = true e.deleteY = e.shape?.position.y break } } } for e in elements { if e.delete { initElement(e: e) } } } if count1 > 0 { addPoint(p: count1) }まとめ
ソースは GitHub にアップしています。