- 投稿日:2020-02-18T20:34:31+09:00
生年月日の入力で、絶対に使っていけないデザイン。カレンダー型のDatePickerについて
- 投稿日:2020-02-18T19:12:09+09:00
コーディング中の警告解消時の注意点
はじめに
みなさん!コーディング時の警告はちゃんと解消してますよね?
ただ、「はいはいいつものやついつものやつ」とか「またこれねハイハイ」とか容易にソースを修正してませんか?
特にテストやらなんやら終わった後のクラスとかで「あれ?警告でてるじゃん、消しとこ」とかで軽い気持ちで修正しちゃったりとかありませんか?
その修正が思わぬ不具合につながるかもしれませんよ?
まえおき
理解してる人はあたりまえの話なのでそこはあたたかい目で読んでいただければと思います
例を作るにあたり、いろいろ突っ込みどころがあると思いますがご容赦を・・・!
ほんだい
極端な例ですが、例えばこんなclassがあったとします。
public class TestClass { TestClass() { hogeInt hogeInt = new hogeInt(); ... if (特定の条件) { Log.d("TAG", "前"); Boolean flg = hogeInt.getIntParam().equals(1); Log.d("TAG", "後"); Toast.makeText(context,"サーバーに truncate table * のクエリを投げました",Toast.LENGTH_LONG).show(); } ... } } class hogeInt { private Integer intParam = null; @Nullable public Integer getIntParam() { return this.intParam; } public void setIntParam(Integer intParam) { this.intParam = intParam; } }そして特定の条件内に入るのが極稀なケースだったとします。
ここでひとつPointなのですが、これを何もせずに実行した場合、
Toast.makeText(context,"サーバーに truncate table * のクエリを投げました",Toast.LENGTH_LONG).show();が実行される前にクラッシュします。
( hogeInt.getIntParam() はNull なのに equals を実行しようとしているので)そして本題。
Boolean flg = hogeInt.getIntParam().equals(1);こいつは警告として表示されます。
今回だと「’NullPointerException’を生成する可能性があります」ってやつですね。わりかしよく見かける部類の警告だと思います。これをアプリ改良中に「あれ?ここ警告でてるからついでに修正しておこう」とか「そういえばたまにクラッシュ報告あるけどこいつかも」
といった軽い感じで何も考えずにソースを修正した場合ソースがこう変化します
public class TestClass { TestClass() { hogeInt hogeInt = new hogeInt(); ... if (特定の条件) { Log.d("TAG", "前"); if (hogeInt != null) { Boolean flg = hogeInt.getIntParam().equals(1); } Log.d("TAG", "後"); Toast.makeText(context,"サーバーに truncate table * のクエリを投げました",Toast.LENGTH_LONG).show(); } ... } }今までクラッシュしていた部分をnullチェックでスルーする事によって後続が流れるようになり、めでたくサーバにTruncate文が投げられる事になりました。
ちなみに今回のお話は警告は残しておいてもいいよ!という事ではなく、小さな警告を消す場合でも慎重になれという事です。
何かしら修正を入れる場合、その一文だけを見ずに、全体の流れをちゃんと把握した上で修正しましょう!
- 投稿日:2020-02-18T11:57:26+09:00
何もしてないのに READ_PHONE_STATE が付与されてしまった!(解決)
まあ何もしてない訳は無いんですよね。
いくつかライブラリをバージョンアップしていました。
1行ずつ戻しながら検証しました。build.gradleimplementation 'com.android.installreferrer:installreferrer:1.0' ↓ implementation 'com.android.installreferrer:installreferrer:1.1.1'これが原因でした。
https://blog.danlew.net/2020/02/11/install-referrer-library-permissions-snafu/
こちらで詳細を見つけました。
とりあえず 1.0 に戻して解決!
- 投稿日:2020-02-18T11:36:10+09:00
Androidの戻るキー対応をなるべく簡単にする提案
意外と面倒くさいAndroid戻るキー対応
ね。面倒くさいですよね。(iOSには無いわけですし・・・)
さしあたり一番簡単なのは「画面上の『ネガティブ的』なボタンと同等の機能をつける」事です。ダイアログを開いたのであれば、戻るキーが「閉じるボタン」や「戻るボタン」相当になればよいですし。
画面遷移したのであれば、戻るキーが「Backボタン」(前の画面に戻る)相当になればよいわけです。
そう書くと簡単のように聞こえますが、
画面遷移した次の画面で、ダイアログを表示した場合は?
戻るキーを押したら、ダイアログも閉じてしまう+画面も前の画面に戻ってしまう
では困るわけです。
これをまともに対応しようとすると、
- 優先順位スタックマネージャ的なクラスを作成
- 画面遷移したら、「戻るボタン」の処理(Actionとか?)を(上記)スタックマネージャにPush
- ダイアログを開いたら「ダイアログ閉じるボタン」の処理(Actionとか?)をスタックマネージャにPush
- 戻るキーを押したら、スタックマネージャにスタックされている処理の一番上(Peek)を処理
- ダイアログを閉じたら「ダイアログ閉じるボタン」の処理をスタックマネージャからRemove
- もう一度戻るキーを押したら・・・・
といった、管理が必要になります。 はい面倒臭いですね!
もっとシンプルに考える
そもそも、上記例の「画面遷移した次の画面で、ダイアログを表示した場合」って、普通はダイアログがモーダル的に表示されていて、後ろの「戻るボタン」は押せないようにしているのがほとんどのはず。(わざわざ後ろのボタンのintaractiveをfalseにしているのか、「タッチガード」的な全画面Panelを一枚噛ませてタッチイベントを遮断しているかのどちらかがほとんどでしょう)
問題なのは、
単純に戻るキーとボタンが押されたときの処理を関連付けてしまうと、uguiのイベントとは関係無しに処理が呼ばれてしまう
ことです。
なので、徹底的にuguiのイベントを倣い、戻るキーの押下を指定ボタンへのマウスクリックへとすり替えてあげれば解決です。
作ってみた
KeyBind.csusing System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; [RequireComponent(typeof(Button))] public class KeyBind : MonoBehaviour { [SerializeField] private Button _targetButton; public KeyCode _bindKey; private void Reset() { _targetButton = GetComponent<Button>(); } private static List<RaycastResult> raycastResultList = new List<RaycastResult>(); private PointerEventData _pointerEventData; private void Update() { //指定したキーの押下 if (Input.GetKeyDown(_bindKey)) { _pointerEventData = new PointerEventData(EventSystem.current) { button = PointerEventData.InputButton.Left, position = _targetButton.transform.position //指定したボタンの位置にマウスがある体 }; EventSystem.current.RaycastAll(_pointerEventData , raycastResultList); var validGameObject = raycastResultList.Select(result => result.gameObject).FirstOrDefault(gameObject => gameObject != null);//一番最初にぶつかっている有効なGameObject取得 raycastResultList.Clear(); if (validGameObject == null) { return; } var currentPointerDownHandlerObject = ExecuteEvents.GetEventHandler<IPointerDownHandler>(validGameObject); //ボタン位置にあるGameObjectからIPointerDownHandlerを保持しているGameObjectを取得 if (currentPointerDownHandlerObject != _targetButton.gameObject){ return; //ボタン位置から得られたGameObjectとボタンのGameObjectが異なる=別のもので遮られている ので処理しない } _pointerEventData.pointerPress = currentPointerDownHandlerObject; ExecuteEvents.Execute(currentPointerDownHandlerObject, _pointerEventData, ExecuteEvents.pointerDownHandler); } //指定したキーの押上 if (_pointerEventData != null && _pointerEventData.pointerPress != null && Input.GetKeyUp(_bindKey)) { ExecuteEvents.Execute(_pointerEventData.pointerPress, _pointerEventData, ExecuteEvents.pointerUpHandler); ExecuteEvents.Execute(_pointerEventData.pointerPress, _pointerEventData, ExecuteEvents.pointerClickHandler); _pointerEventData = null; } } }(よくわからんなりに調べて作ったので、大分力業ですが・・・)
使い方
このScriptを
Button
コンポーネントが乗っているGameObject
に追加します。
Target Button
は勝手に同GameObject
のButton
がセットされます。
そして
Bind Key
には割り当てたいハードキー を指定します(KeyCode
の一覧が候補で出ます)
Androidの戻るキーは KeyCode.Escape で割り当たります。
なんと、これだけで、ボタンのタップとAndroidの戻るキーが同等になります! シンプル!!
Androidの戻るボタン対応
— すずきかつーき (@divideby_zero) February 18, 2020
1回目は、普通にマウスでダイアログの「戻るボタン」と画面の「バックボタン」をクリック、2回目はAndroidの戻るボタンによる処理。
”クリックできるボタンのみ反応"するので、お手軽Android戻るキー対応としてはありなんじゃないかと。
解説はこちらhttps://t.co/je9qlPXJ7V pic.twitter.com/DfkPELJYqp注意
ボタンを疑似的にクリックした相当なので、(利点でも欠点でもあるんですが)ボタンのTransitionがそのまま効きます。
↑の動画をよく見ると分かるんですが、戻るキーを押した時でもボタンの色が変化しています(戻るキーを押しっぱなしにすると、ボタンも押されっぱなしになる)
それが嫌! という場合には使えないです。 悪しからず・・・。補足
今回、 Androidの戻るキー対応 と銘打っては居ますが、既に書いた通りボタンには KeyCodeで割り当てるキーを指定することができます。
もう一つの使い道として、入力処理の一元化があります。初めてUnity(2019.3)使うので、練習としてテトリス?っていうのを作ってみました。
— すずきかつーき (@divideby_zero) February 13, 2020
聞くところによると「Tスピン」というのが強いらしいので見様見真似で実装してみましたが、回転力を強くしすぎてしまったようです。#madewithunityhttps://t.co/7XjEMvHJLx pic.twitter.com/us0kXPFT4Gこちらの動画のゲームで今回のKeyBind.csが実際に使われており、前半はマウスでボタンをクリックして操作ですが、後半はそれぞれのボタンに割り当てられたキーボードで操作しています。
このように「複数方法の入力処理を制御」するには
- ベタでボタンがクリックされた場合の処理とキーボード入力の処理の場合を分けて書いてしまうスタイル
IInput
のような入力処理を抽象化したインタフェースを切り、IInput
を実装したKeyboardInput
とButtonInput
のようなクラスをそれぞれ実装するスタイル- 神Inputクラスに想定される全インプット処理分の条件分岐をぶち込んでいくGODスタイル
などなど。方法はありますがそれなりに面倒で。
対して、↑の動画のゲームでは入力制御は「ボタン処理」のみ対象に記述しています。
そして、KeyBind.csはあくまでもボタンの疑似クリック処理なので、キーボード操作を増やしても入力制御処理は何も手を入れずに済んでいます。 つまり、ボタンによる入力処理で「処理の一元化」がされている状態です。もちろん、これは画面上にバーチャルパッド的なものをuguiで置いているから出来るだけなので適用範囲はそう広くは無いですが、使える人も少なくないのではないでしょうか。
- 投稿日:2020-02-18T01:28:35+09:00
jetpack navigationを使ってViewPager2上のFragmentから別Fragmentに画面遷移した後、navigateUpをするとStateAdapterがあれで落ちる話
どういうこと
まずはこれを見てください。
自作Connpassのイベント一覧表示するやつ
お勉強も兼ねて、ちくちく作ってるアプリです。このアプリのメイン画面は一覧表示のFragmentを2つもっており、それらをViewPager2で保持しています。
(ちなみにFragmentStateAdapterはdaggerでInjectしています)
一覧表示のアイテムをクリックして詳細画面へ遷移する腹積もりです。
そして、詳細画面でfindnavcontroller().navigateUp()をしたときに掲題の件にぶつかります。
構成イメージ
App ├ MainFragment │ ├ List1Fragment │ └ List2Fragment │ └ DetailFragmentどんなエラーがでるのかその①
単純に上記のプログラムを実行して、一覧画面->詳細画面->戻るボタンを実行すると下記クラッシュを起こします。
2020-02-18 00:37:34.858 4022-4022/com.ryunen344.connpasssearch.debug E/AndroidRuntime: FATAL EXCEPTION: main Process: com.ryunen344.connpasssearch.debug, PID: 4022 java.lang.IllegalArgumentException at androidx.core.util.Preconditions.checkArgument(Preconditions.java:36) at androidx.viewpager2.adapter.FragmentStateAdapter.onAttachedToRecyclerView(FragmentStateAdapter.java:132) at com.ryunen344.connpasssearch.feature.main.MainFragmentStateAdapter.onAttachedToRecyclerView(MainFragmentStateAdapter.kt:32) at androidx.recyclerview.widget.RecyclerView.setAdapterInternal(RecyclerView.java:1209) at androidx.recyclerview.widget.RecyclerView.setAdapter(RecyclerView.java:1161) at androidx.viewpager2.widget.ViewPager2.setAdapter(ViewPager2.java:461) at com.ryunen344.connpasssearch.feature.main.MainFragment.initViewPager(MainFragment.kt:43) at com.ryunen344.connpasssearch.feature.main.MainFragment.onViewCreated(MainFragment.kt:35) at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:322) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1185) at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2222) at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1995) at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1951) at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1847) at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413) at android.os.Handler.handleCallback(Handler.java:883) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7356) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)FragmentStateAdapter#onAttachedToRecyclerViewで落ちていますね。
何をしているかというと// FragmentStateAdapter.java @CallSuper @Override public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { checkArgument(mFragmentMaxLifecycleEnforcer == null); mFragmentMaxLifecycleEnforcer = new FragmentMaxLifecycleEnforcer(); mFragmentMaxLifecycleEnforcer.register(recyclerView); } // Preconditions.java public static void checkArgument(boolean expression) { if (!expression) { throw new IllegalArgumentException(); } }こうなっています。
mFragmentMaxLifecycleEnforcer がMainFragmentとFragmentStateAdapterのLifecycleを紐づけていて、
一覧から詳細画面に遷移した際に一覧のFragmentは破棄されないため、mFragmentMaxLifecycleEnforcerはnon nullとなっています。
落ちます。どんなエラーがでるのかその②
自分はFragmentStateAdapterのonDetachedFromRecyclerViewが呼ばれていないから起こるのではと予想し、下記コードをMainFragment.ktに追加しました。
Fragment#onDestroyViewのタイミングでViewPager2へのadapterの参照を外してあげています。MainFragment.ktoverride fun onDestroyView() { binding.viewPagerContainer.adapter = null super.onDestroyView() }するとこうなる。
2020-02-18 00:59:17.535 4455-4455/com.ryunen344.connpasssearch.debug E/AndroidRuntime: FATAL EXCEPTION: main Process: com.ryunen344.connpasssearch.debug, PID: 4455 java.lang.IllegalStateException: Expected the adapter to be 'fresh' while restoring state. at androidx.viewpager2.adapter.FragmentStateAdapter.restoreState(FragmentStateAdapter.java:536) at androidx.viewpager2.widget.ViewPager2.restorePendingState(ViewPager2.java:350) at androidx.viewpager2.widget.ViewPager2.dispatchRestoreInstanceState(ViewPager2.java:375) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3864) at android.view.View.restoreHierarchyState(View.java:19793) at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:573) at androidx.fragment.app.FragmentStateManager.restoreViewState(FragmentStateManager.java:346) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1187) at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2222) at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1995) at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1951) at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1847) at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413) at android.os.Handler.handleCallback(Handler.java:883) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7356) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)どういうこっちゃ
Expected the adapter to be 'fresh' while restoring state
ふーむ、Adapterが再生成されてないことに怒っているのかな・・・?それもそのはず。
一覧画面 -> 詳細画面への遷移を行ってもMainFragmentは破棄されていないので当然FragmentStateAdapterも破棄されていない。
だが、onCreateViewは走るのでViewPagerとViewPager内のRecyclerViewは再生成されている。
再生成されたRecyclerViewに対して前の状態を保持しているFragmentStateAdapterをrestore, attachしようとすると参照が噛み合わないので落ちてる(と思います)。あれ!?FragmentStateAdaterって再生成されてないの!?!?
MainFragmentは破棄されていないので当然FragmentStateAdapterも破棄されていない。
先述のこれ、後になって気付きました。じゃあ毎回FragmentStateAdapterのコンストラクタを走らせるしかあるまい・・・・と思い、DaggerさんにSpring Frameworkでいう@Scope("prototype")
のやつちょーだいって書くことにしました。
(別にDIする必要ないんですが、できるものならなんでもDIしたいお年頃なので・・・・)MainFragmentModule.kt// includesにProvidersを追加 @Module(includes = [MainFragmentModule.Providers::class]) abstract class MainFragmentModule { @FragmentScope @ContributesAndroidInjector(modules = [EventListFragmentModule::class]) abstract fun contributeEventListFragment(): EventListFragment @FragmentScope @ContributesAndroidInjector abstract fun contributeSearchFragment(): SearchFragment // includesにProvidersを追加 @Module internal object Providers { @Provides internal fun provideMainFragmentStateAdapter(mainFragment: MainFragment) = MainFragmentStateAdapter(mainFragment) } }書いて安心してはいけません。DaggerはComponentの死活が再生成と同義です。
つまりこのスコープではFragmentStateAdapterは死なないので再生成されないのです。えっ、だからSpring Frameworkでいう
@Scope("prototype")
はどうやるんだってばよFragmentStateAdapterのobjectをinjectするのではなくMainFragmentModule.Providersをinjectすることによって実現しました。
つまりこうなります。MainFragment.kt// objectをinjectしていた部分を下記に書き換える @Inject lateinit var mainFragmentStateAdapterProvider: Provider<MainFragmentStateAdapter> ~ ~ ~ ~ ~ // binding.viewPagerContainer.adapter = mainFragmentStateAdapter // これを binding.viewPagerContainer.adapter = mainFragmentStateAdapterProvider.get() // こうするこうすることでProviderが毎回FragmentStateAdapterのinstanceを生成してくれます。
※onDestroyView()でviewpager.adapter = null
するといいと思います。終わりに
daggerは俺に微笑んでくれない、誰も俺を愛してくれない・・・・・・
koinは俺を愛してくれるだろうか・・・・・
- 投稿日:2020-02-18T01:00:39+09:00
[Android]Screen に Viewが表示されたか判定する
はじめに
View が Screen に表示されたか判定する処理を作成します。例えば Player が Screen に表示されたら動画の再生を開始したい、TextView が表示されたらテキストカラーを変えたい、といった処理に使えるサンプルを作成してみたいと思います。
TL;DR
表示されたか判定する方法はシンプルです、 Screen の Rectと View の Rect を取得して、 Screen の Rectに View のRect が含まれているか判定するだけです。
1. Screen の範囲(Rect)を取得する
MainActivity.ktprivate fun getScreenRect() : Rect { val displayMetrics = DisplayMetrics() windowManager.defaultDisplay.getMetrics(displayMetrics) return Rect(0, 0, displayMetrics.widthPixels,displayMetrics.heightPixels) }2. View の範囲(Rect)を取得する
MainActivity.ktprivate fun View.getViewRect() : Rect { val array = IntArray(2) this.getLocationOnScreen(array) return Rect(this.width, this.height, array[0], array[1]) }3. Screen に View に表示されているか判定する
MainActivity.ktscroll_view.viewTreeObserver.addOnScrollChangedListener { val screenRect = getScreenRect() val oneRect = one.getViewRect() if (screenRect.contains(oneRect)) { // 含まれているときの処理 } else { // 含まれていないときの処理 }レイアウトの準備する
判定処理を記述するためにスクロールしなければスクリーンに表示できないようなレイアウトを作成します。ScrollView の中に TextView が3つ含まれているレイアウトを準備しておきましょう。
activity_main.xml<ScrollView android:id="@+id/scroll_view" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/linear_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/one" android:layout_width="match_parent" android:layout_height="200dp" android:textAlignment="center" android:textSize="80sp" android:text="One" /> ︙ </LinearLayout> </ScrollView>Screen の範囲(Rect)を取得する
Screen の位置(サイズ)は
windowsManager.defaultDisplay.getMetrics()
に取得できます。このgetMetrics
で取得した幅と高さを利用して、Screen の範囲を示すRect
オブジェクトを作成しましょう。(ここで取得できるサイズは Pixel になります)MainActivity.ktprivate fun getScreenRect() : Rect { val displayMetrics = DisplayMetrics() windowManager.defaultDisplay.getMetrics(displayMetrics) return Rect(0, 0, displayMetrics.widthPixels,displayMetrics.heightPixels) }View の範囲(Rect)を取得する
View の左上の座標を
View.getLocationInWindow
で取得できます。また View の高さと幅はView.width
とView.height
で取得できます。これらの値を組み合わせて View が表示されている範囲を示すRect
オブジェクトを作成しましょう。(ここも取得できるサイズは Pixel になります)MainActivity.ktprivate fun View.getViewRect() : Rect { val array = IntArray(2) this.getLocationOnScreen(array) return Rect(this.width, this.height, array[0], array[1]) }Screen に View に表示されているか判定する
あとは ScrollView のスクロール位置が更新された時、Screen に View が表示されているか判定する処理を記述すれば完成となります。判定処理を記述する箇所は
ScrollView
のviewTreeObserver
にaddOnScrollChangedListener
になります。そこに Screen の範囲(Rect)に View の範囲(Rect)が含まれるか判定する処理を記述していきます。(含まれるか判定するには Rect の contains が便利です、含まているかそうでないかを Boolean で返すメソッドになります。)MainActivity.ktoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val oneTextView = one val twoTextView = two val threeTextView = three scroll_view.viewTreeObserver.addOnScrollChangedListener { val screenRect = getScreenRect() val oneRect = one.getViewRect() val oneColor = if (screenRect.contains(oneRect)) { Color.RED } else { Color.WHITE } oneTextView.setBackgroundColor(oneColor) val twoRect = twoTextView.getViewRect() val twoColor = if (screenRect.contains(twoRect)) { Color.RED } else { Color.WHITE } twoTextView.setBackgroundColor(twoColor) val threeRect = threeTextView.getViewRect() val threeColor = if (screenRect.contains(threeRect)) { Color.RED } else { Color.WHITE } threeTextView.setBackgroundColor(threeColor) } }参考文献
- 投稿日:2020-02-18T00:12:14+09:00
「Swift」でマルチプラットフォーム開発をしよう!
前置き
この記事自体は2019年10月に書き終わっていたのですが、、、
DroidKaigi2020に応募したCFP
「Scadeを使って「Swift」で始めるAndroidアプリ開発」
が通ったため、だいぶ温めてからの公開になりました、、笑残念ながら開催は見送られてしまいましたが、、、
開催週に登壇の前座資料として、元々この記事を公開予定だったので公開しています!
(登壇資料はまだ開催予定を信じて公開は致しませんmm)内容
登壇内容ほど詳しくは載せていませんが、みなさんがSwiftを使ってScadeでアプリ開発できるように
- Scadeのライトな説明 - 環境設定方法 - 開発トラベルシューティング周りを記載しています。
What is Scade ?
SwiftをベースにiOS/Android向けのアプリを開発できる統合開発環境です。
どんな物が作れる?
DroidKaigi2020のコピーアプリをScadeを使って作りました。
といっても、検索フィルターなど一部の画面・機能は未実装です。
(単純に自分のコミット不足で実装しきれなかった)また、リリースしたいと考えていたので(結局してないけど笑)
申請のリジェクトリスクを恐れてデザインは少し変更しています。
デザイン自体はDroidKaigi2020のアプリをベースにしています。アプリのリポジトリを用意してあるので、詳しくはそのReadmeを見れいただけると!
(一部動作はぬるっとしてたりします。特にAndroidは、、泣)
開発環境構築
公式サイトにあるものは、英語なのと古いので、改めて以下の手順で行うと良いかと。
1. インストール
多いですが、iOS/Android双方へアプリをビルドする必要があるため、以下を全てインストールします。
- Java 8 SDKのダウンロード(ミラー)
- Android NDKのダウンロード(ミラー)
- Android Studioのダウンロード
- Xcodeのダウンロード(Xcode11以降)
- Scadeのダウンロード
複数のJREがインストールされている場合は、指定してあげるための対応が必要です。
(上記の公式サイトを参照してください)2. Androidでビルドできるようにする
① Android Studioを起動
プロジェクトを開くか or 空のプロジェクトを作成してください
② 対応バージョンを入れる
[AndroidStudio] > [Preferences] > [Android SDK]
と進み、Android 6.0 にチェックを入れてインストールします。③ 必要なSDKをインストールする
いくつかはデフォルトでインストールされているが、以下はないのでインストールします。
- Google Play services
- NDK④ シュミレータのインストール
Android Studioをインストールしただけでは、何もシュミレータが入っていないので、自分で入れる必要があります。例では、Pixelを選択していますが、好きな端末を選んでインストールしていただければ。
3. iOSでビルドできるようにする
(こちらは後で行うことになるので一旦飛ばしても大丈夫です)
Xcode周りでは特に設定は必要ないです。
ただ、証明書がないとビルドできません。
(回避方法があるかも?現状見つけていない)Scadeのプロジェクト作成時にできるbuild.yamlに証明書を設定する部分があるので設定します。
(証明書ファイル名前は適当ですので、確実すきな名前にしてください)とりあえず動かしたいという方は、証明書を作成する際にApple Developerでアプリ名をワイルドカードにすると良いでしょう。
4. Scadeのセットアップ
1. パスの設定をする
Scadeの設定画面から以下を設定します。
- Android SDK はUsers配下にあるので、各自のパスを指定
- Android NDK は最初にインストールしたものを任意の場所に設置して、その場所を各自が指定
Android SDKの場所はAndroid Studioの設定画面からパスを確認することができます。
5. 「HelloWorld」する
XcodeのGUIのように開発が可能です!
①新規プロジェクトの作成
②ラベルの設置
③ビルドする端末を選択
④HelloWorld!
設定は少し面倒ですが、起動してしまえばすぐに開発を始めることができます。
※初回ビルドは失敗するので2回ビルドすること(端末が起動前だとエラーになる。特にAndroid)
※同じ端末をマルチビルドできないため、1端末ごとで行いましょうその他
開発環境トラブルシューティング
実際に出くわして解決したものを載せておきます。
1. Androidでビルドできない時
DLツールの許可が必要な場合
インストールしたNDK内のコンパイラがビルド最中に弾かれてしまう場合があります。
(外部MacOS Catalinaだと発生する模様)その場合は一度キャンセルし、Mac本体の
システム環境設定 > セキュリティとプライバシー
から許可を行ってください。
(「ゴミ箱に入れる」は絶対にしない でください。やってしまった場合は、NDKを再インストールして入れ替えてください)ちなみに4つほど弾かれるので、
許可→再ビルド
というのを4回ほど行う必要があるので注意サポートライブラリが読み込めない場合
普通にAndroidの開発をしていた場合でも発生するエラーですが、、
(Androidでのビルドが止まってしまう)実際にScade上で発生するエラーは以下
プロジェクトをクリーンをして、以下のandroidディレクトリを削除し
(Library > Android > sdk > android
)Scadeでビルドし直すことで解決できます。
そうすることで、サポートライブラリが再インストールされます。
(ただし、数分時間がかかるので注意)2.iOSでビルドできない時
Scadeのアップデートの影響
最新版のScadeにアップデートした際に、Xcodeを10系->11系にする必要があります。
(現状Scadev0.9.17以降はXcode11.x系でないとビルドできなくなった)その際、プロジェクトファイルのbuild.yamlファイルが変わっているためにビルドできなくなります。
アップデートすべき項目をこちらです。新規プロジェクトを作成し、その中にあるyamlファイルを参考に古いプロジェクトにも項目を追加すると良いでしょう。
3.ビルドが通らない時
コンパイルが通らない
Xcode/Android Studio同様に、Scadeではビルド高速化のために、一度ビルドしたものをキャッシュしています。
が、その部分がたまに壊れてしまいビルドできなくなることがあります。プロジェクトをクリーンし、吐き出されている[products]配下をすべて削除して、再びビルドすると通ることが多いです。
インストールされない・起動しない
Androidでよく起こりますが、端末によってはインストールできないアーキテクチャがあるようなので、
x86, arm64などいずれかでビルドできないか色々試してみましょう。また、XcodeやAndroid Studioでもあるあるですが、再起動やプロジェクトクリーンで起動することもあります笑
git管理
リポジトリが肥大化しないために、gitignoreに以下を追加します。
(書き出されたipa, apkが含まれないようにします).build/ .target/ products/ *.ipa *.scadeapp .DS_Store CMakeLists.txt fonts/終わりに
「タイトルにSwiftってあるのに記事に1文字もSwiftコードねえじゃねえか!」
すみません、書き終わって気づきました笑
(登壇の前座資料なので、あまり触れるわけにもいかず、、)実際にどんな感じに書いているのかは、作成したアプリのリポジトリをみていただくか、
公式サイトにあるサンプルコードリポジトリをみていただくのが良いかと!iOSとAndroidで実装分けるために汚いコードも一部ありますが、、、
それも含めてご覧になっていただけると笑