20200801のiOSに関する記事は14件です。

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から知りたい人は、こちら:point_down:を参考に
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って何やねん?」と言う方は:point_down:こちらを参考に
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.ts
import { 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.ts
import { AdmobService } from '../service/admob.service';

constructor(private admobService: AdmobService) {}

ionViewWillEnter() {
    this.admobService.showBanner();
  }

変わった点

hasTabBartabBarHeightがなくなり、marginになったぐらいかな?

capacitor-admob:point_down:

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プラグイン:point_down:

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
このページを丸々翻訳にかけると、ある程度は理解できるかと:wink:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutterの状態管理】テーマ切替&多言語化 〜provider,BLoC,reduxの3パターンで実現してみる②

お待たせしました。プロジェクトの人員異動でバタバタしていました。
さて、前回ではproviderという実現方式でflutterの状態管理を説明しました。今回はBLoC(Business Logic Component)を利用し、アプリのテーマ(Theme)切替、多言語化を通じてFlutterの状態管理を説明します。

二、BLoCでテーマ切替&多言語化

Blocには3つのポイントがある。それは 業務ロジックユニット状態イベント

flutter_bloc: ^6.0.1

color1.png color2.png color5.png color6.png language3.png language4.png

1.テーマ切替のBloc

ハンバーガーメニューにあるテーマ色ボタンの押下により、グローバルにテーマ色を切替える。

1.1状態クラス

テーマ関連の状態量を抽象クラスに置きながら、他の状態はこれを継承し状態の細分化を実現する。

theme_state.dart
import '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.dart
import '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.dart
import '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.dart
import '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.dart
import 'package:flutter/material.dart';

@immutable
abstract class BlocLocaleEvent {}

class BlocEventSwitch2JP extends BlocLocaleEvent{}

class BlocEventSwitch2EN extends BlocLocaleEvent{}

2.3業務ロジッククラス

lang_bloc.dart
import '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.dart
import '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.dart
import '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パターンをご紹介しますので、引き続きお楽しみください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebViewの表示方法

#WebViewをサクッと表示!!

まず、ViewController.fileにWebKitをimportします。

ViewController.swift
import UIKit
import WebKit

class ViewController: UIViewController {
    @IBOutlet weak var webView: WKWebView!

    override func viewDidAppear(_ animated: Bool){
        }
    } 

次に、main.storyboardへ。
WebKit Viewを右上の「+」 ボタンから選択し、ViewControllerへ貼り付け。

スクリーンショット 2020-08-01 21.04.34.png

次にwebViewを表示させるコードを書きます。

ViewController.swift
import 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)
}

これでオッケーです!
ただ、これでシュミレーターを立ち上げると・・・

スクリーンショット 2020-08-01 21.20.41.png
この様に、エラーが出てしまいます。
どうやら原因はフレームワークが追加されていないからとのこと。
ということで、追加。
スクリーンショット 2020-08-01 21.03.11.png

これで完成です!
webViewを表示するのは非常に簡単ですね!
ビューアーなどの記事サイトを表示させる時に活躍しそうですね。
webkitViewの使用方法でした!ありがとうございました。
            スクリーンショット 2020-08-01 21.25.19.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftの実機テストする際に出たエラーの解決法

はじめに

いつもはSimulatorを使ってテストをしているのですが、自分のiPhoneでも試してみたいと思い、実機テストをやってみました。思っていたより梃子摺ったので遭遇したエラーの解決方法をまとめてみました。

「Signing for "アプリ名" requires a development team.」というエラーが出た場合

Signing & CapabilitiesでTeamを設定する。Apple IDでログインするとTeamという項目で自分のアカウントを選択できるようになるのでそこから設定する。
スクリーンショット 2020-08-01 20.39.24.png

「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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RiverpodとFlutter Hooksを使って、カウンターアプリを作ってみよう

はじめに

この記事では、RiverpodとFlutter HooksとStateNotifierを使って、
カウンターアプリを作ってみようと思います。

今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_example

スクリーンショット 2020-08-01 14.34.35.png

Flutterの状態管理パターン

みなさん、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とFlutterHooksを使ってカウンターアプリを作ってみましたので、当記事にまとめます。

Riverpodとは

Providerの作者による、Providerの進化版です。

Provider, but different

Riverpod公式ページ
https://riverpod.dev
スクリーンショット 2020-08-01 14.36.53.png

「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/98d1bc4a83265a764084

https://riverpod.dev
Riverpodの公式ページを見る限り、RiverpodとFlutter Hooksはセットで使うのが、作者の推しみたいです。
RiverpodはFlutter Hooksを外しても利用できます。

カウンターアプリを作ってみよう

Riverpod と Flutter Hooks と StateNotifierインストール

pubspec.yaml
environment:
  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.0

pubspec.yamlに上記のように書いて、ターミナルで「flutter pub get」しましょう。

flutter pub get

これでインストールは完了です。

それでは、コードを書いていきましょう。

lib/main.dart
import '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.
      ),
    );
  }
}

めちゃくちゃコードがすっきり書けて感動。

実行して、「+」を押すと数字がカウントアップすると思います。
スクリーンショット 2020-08-01 14.34.35.png

コードの説明

グローバルに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

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RiverpodとFlutter Hooksを使う、はじめの一歩

はじめに

この記事では、RiverpodとFlutter Hooksを使う、はじめの一歩として、
カウンターアプリを作ってみようと思います。

今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_example

スクリーンショット 2020-08-01 14.34.35.png

Flutterの状態管理パターン

みなさん、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とFlutterHooksを使ってカウンターアプリを作ってみましたので、当記事にまとめます。

Riverpodとは

Providerの作者による、Providerの進化版です。

Provider, but different

Riverpod公式ページ
https://riverpod.dev
スクリーンショット 2020-08-01 14.36.53.png

「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/98d1bc4a83265a764084

https://riverpod.dev
Riverpodの公式ページを見る限り、RiverpodとFlutter Hooksはセットで使うのが、作者の推しみたいです。
RiverpodはFlutter Hooksを外しても利用できます。

カウンターアプリを作ってみよう

Riverpod と Flutter Hooks と StateNotifierインストール

pubspec.yaml
environment:
  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.0

pubspec.yamlに上記のように書いて、ターミナルで「flutter pub get」しましょう。

flutter pub get

これでインストールは完了です。

それでは、コードを書いていきましょう。

lib/main.dart
import '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.
      ),
    );
  }
}

めちゃくちゃコードがすっきり書けて感動。

実行して、「+」を押すと数字がカウントアップすると思います。
スクリーンショット 2020-08-01 14.34.35.png

コードの説明

グローバルに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

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

selfって書いたり書かなかったりすることない???

以前働いていた会社ではソースコードで絶対にselfをつけていたのに、ウェブで読むソースコードではほとんどselfがついてないことが多い。たまにエラーが出るからつけときましょう、っていうのが多い。

なんで?つけるのつけないのどっち???

下記サイトからわかりやすい文章を参照。
https://medium.com/@muukii/swift-%E3%83%A1%E3%83%B3%E3%83%90%E5%A4%89%E6%95%B0%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E6%99%82%E3%81%ABself%E3%81%AF%E6%9B%B8%E3%81%8F%E3%81%B9%E3%81%8D-7a1b1069bf9d

Swiftはselfを書かなくてもメンバ変数にアクセスが可能です。
ただ、ローカル変数と衝突した際にはselfを付ける必要があります。

なるほど〜〜〜。

上記サイトにもメリットデメリット載っており、意見を書かれているので、ここでの結論としては、

Xcodeに怒られたらつけとこ〜〜〜🐣⭐️🐣⭐️🐣⭐️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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/7075fb911da5c9d2fba5

UIView(+それを継承しているUIImageView等)のプロパティである
Viewにセットしたコンテンツが、領域boundsの外を描画するかどうかを決定する
デフォルト値がfalseで、trueにすると領域内に限定される

よくわからない・・・

と思い、下に読み進めていくと、わかりやすい画像が載っていた。
ここで記載するのは割愛しますが、わかりやすくいうと、imageViewを配置して画像をセットしたとき、
false: 画像がはみ出した状態で出力
true: 画像をimageView内に収める
って感じです。

つまり、画像の角の数値を決めたりとかするためにclipsToBoundsをtrueに定義しているわけではなく、丸いimageViewを決めた後に、アイコンをセットした際、その枠内に当て嵌めるようにしてるんですね。ナットクトクコさん。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

全く需要が無さそうだが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.swift
import 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.swift
import 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.swift
NavigationLink(
    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を編集する必要があるので注意してください。

既知の問題

筆記体のヘブライ文字はイタリックの文字のように右に傾いており、上下に幅の広い文字の場合、右上と左下に大きく伸びます。これが表示上左右の出っ張りが切れて見えなくなるという問題があります。ちなみに左右にスペースを入れても直りません。今回解決できなかったので、左右に_を入れて対処しました。

Screen Shot 2020-08-01 at 13.19.09.png
これがアレフ。

Screen Shot 2020-08-01 at 13.19.49.png
ちょっと右が切れてるのわかります?

Screen Shot 2020-08-01 at 13.21.33.png
これがギメル

Screen Shot 2020-08-01 at 13.21.07.png
あー右が切れた。

LetterExplanation.swift
ScrollView{
    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.swift
Button(action: {
    self.showingSetting.toggle()
}) {
    Image(systemName: "gear").imageScale(.large)
}.sheet(isPresented: $showingSetting) {
    SettingView(
        showSheetView: self.$showingSetting// userData: self.userData
    )
    .environmentObject(self.userData)
}
SettingView.swift
import 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になんか書こうと思ってたのですが、いろいろ引っ張ってきてたリンクをもうたどることができない...。みんなどうやって記事書いてるんだ。作りながら書いてる? 今度からは記事を書きながら作業するのが目標になりそう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

仮置きでゴリ押しする、イラストロジック解析アプリ

はじめに

iOS / Android で動作する解析アプリを作りました。
邪道なことは重々承知していますが、イラストロジック雑誌の難問が解けないのは悔しいので。

↓ 実行例
ss_50x50

プロジェクト

プロジェクトはこちら

仮置き

このアプリは解析に行き詰まると、まだ埋まっていないマスに仮置きして解析を続行します。

例えば、あるマスを「黒」と仮定して解析をすすめた結果、ロジックに矛盾が生じたとします。
ロジックが成り立たないということは「起点となったマスは黒であってはならない」ということがわかります。
言い換えると「起点となったマスは白である」となるわけで、仮置きした「黒」を「白」に反転確定させて解析を進めます。

仮置き中に行き詰まると、さらに仮置きをします。
もし、延々と仮置きをしていって全てのマスが矛盾なく埋まった場合は、仮置きが全て正しかったとしてロジッククリアとします。

↓ 仮置きでゴリ押しする様子(仮置きを起点としたマスはムラサキ色で表示)
ss_45x45

思ったこと

サンプル用にイラストロジックの問題をいくつか作ったのですが難しかったです。
仮置きばかり発生して、ロジック的な面白さの皆無な問題ばかり出来上がりました。

ほとんど仮置きせずに解き進められる、雑誌などの絶妙なバランスはすごいと痛感しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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はうまくいった。

もしかしてだけど

明日になったらまた同じことおこる?
それは明日考えよう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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が更新されます。

解説

@PublishedPublisherプロトコル

バインドしたいプロパティの変更を監視するために、import Combineしたうえで、@PublishedというProperty Wrapperをバインドしたいプロパティに付与します。

ViewModel
class 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.text1のようにバインド先のUIViewの型名とそのプロパティ名を記載します。

assignメソッドはAnyCancellableというCancellableプロトコルに準拠したクラスのインスタンスを返します。このインスタンスのcancelメソッドを呼ぶことでバインド(監視)が解除されます。

また、AnyCancellableのインスタンスはメモリから解放されるタイミングでもcancelメソッドを呼んでバインドを解除するようです2。そのため、ViewControllerSet<AnyCancellable>型のプロパティを持たせて参照を保持します。追加する際にチェーンでメソッドを呼べるようにstoreメソッドが用意されています。

map

Publisherには様々なオペレーターが用意されており、監視している値に変更を加えたり、流れてくる時間や回数などの条件によって流れをせきとめたりすることができます。

mapはもっとも基本的なオペレーターで、流れてきた値を加工することができます。
ここでは、UILabel.text: String?の型に合わせて、ViewModel.labelText: StringOptional型に変換しています。

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プロパティのlabelTextBinding<String>という型を返します。これをBinding<String>型を引数にとるTextFieldに渡すことによって、双方向バインディングとなります。

こちらのSwiftUI版では、viewModel.labelTextの値を変更するとTextTextFieldの値が更新されますし、キーボードからTextFieldを変更しても、viewModel.labelText(とText)の値が更新されます。


  1. バックスラッシュはKey-Pathを表現するときの記法です。ドキュメントはこちら Key-Path Expression 

  2. ドキュメントに記述を見つけられなかったのですが、参照を保持しなかった場合はviewDidLoadを抜けた時点でcancelが呼ばれていました。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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が更新されます。

解説

@PublishedPublisherプロトコル

バインドしたいプロパティの変更を監視するために、import Combineしたうえで、@PublishedというProperty Wrapperをバインドしたいプロパティに付与します。

ViewModel
class 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.text1のようにバインド先のUIViewの型名とそのプロパティ名を記載します。

assignメソッドはAnyCancellableというCancellableプロトコルに準拠したクラスのインスタンスを返します。このインスタンスのcancelメソッドを呼ぶことでバインド(監視)が解除されます。

また、AnyCancellableのインスタンスはメモリから解放されるタイミングでもcancelメソッドを呼んでバインドを解除するようです2。そのため、ViewControllerSet<AnyCancellable>型のプロパティを持たせて参照を保持します。追加する際にチェーンでメソッドを呼べるようにstoreメソッドが用意されています。

map

Publisherには様々なオペレーターが用意されており、監視している値に変更を加えたり、流れてくる時間や回数などの条件によって流れをせきとめたりすることができます。

mapはもっとも基本的なオペレーターで、流れてきた値を加工することができます。
ここでは、UILabel.text: String?の型に合わせて、ViewModel.labelText: StringOptional型に変換しています。

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プロパティのlabelTextBinding<String>という型を返します。これをBinding<String>型を引数にとるTextFieldに渡すことによって、双方向バインディングとなります。

こちらのSwiftUI版では、viewModel.labelTextの値を変更するとTextTextFieldの値が更新されますし、キーボードからTextFieldを変更しても、viewModel.labelText(とText)の値が更新されます。


  1. バックスラッシュはKey-Pathを表現するときの記法です。ドキュメントはこちら Key-Path Expression 

  2. ドキュメントに記述を見つけられなかったのですが、参照を保持しなかった場合はviewDidLoadを抜けた時点でcancelが呼ばれていました。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftでパズルゲーム制作を開始した

動機

今まで独自のゲームを作ってばかりでしたが、全然売れなかったので、有名ゲームをパク・・参考にすることにしました。
とりあえず形から入ろうということで、ドラゴンとパズルをテーマにすることにしました。

このテーマで有名なゲームを参考にしていたのですが、「面白いか?」と単純な疑問を抱きました。
もう少し分かりやすいルールのパズルの方が良いのではと思い、別の有名ゲームを参考にしました。
ツムツムかポコパンかよく覚えていませんが、その辺りのゲームのルールが分かりやすそうだったので、パズル部分は隣接するアイテムをなぞって消すルールにしました。

現状

TestFlightでベータ版をテストしている状態です。

ゲームロジック

動きの処理は、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 にアップしています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む