BitriseでGemをキャッシュする方法
BitriseのCache:Pushステップを利用して、Gemをキャッシュさせます。
なお、Bitriseのキャッシュの仕組みについてはこちらの記事で紹介しているので参考にしてください。
Gemをキャッシュさせる方法
こんな構成のワークフローを作成します。
Scriptステップでは以下のスクリプトを実行します。
bundle install envman add --key GEM_HOME --value $(gem environment gemdir)
bundle install
でリポジトリのGemfile
とGemfile.lock
を基にGemをインストールします。
続いて、envman
を使ってGemのインストールディレクトリパスを環境変数GEM_HOME
に登録します。
続いて、Cache:PushステップのCache pathsに$GEM_HOME
を入力します。
こうすることで、次回Cache:PullステップでGemのインストールディレクトリパスがキャッシュからダウンロードされ、Gemのインストール処理が省略されます。
キャッシュが使われるようになったか確認する
キャッシュする前のビルドのログがこちらです。
Installing...
となっていて、インストール処理が行われていることがわかります。
ステップの処理時間も1分ほどかかっていました。
キャッシュしたあとのビルドのログがこちらです。
Using...
となっているので、インストール処理が省略されていることがわかります。
ステップの処理時間も約13秒と大幅に短縮されました。
Knuffを使ってプッシュ通知のテストをする
これまでPusherというツールを使ってプッシュ通知受信のテストをしてたのですが、最近久しぶりに使ってみたらエラーが出てしまいプッシュ通知が送信できなくなっていました。
↓プッシュ通知を送信している様子。
Unable to read: Read connection closed
というエラーが表示されてしまう。
Issueを調べたら同じ問題にあたってる人が結構いたようで、Knuffというツールが使えるという情報を得ました。
Pusherと同じような使い勝手で簡単に使うことができました。
ここではKnuffの使い方をご紹介します。
Knuffのインストール
READMEにダウンロードリンクがありますが、Homebrewでもインストールできました。
$ brew install --cask knuff
プッシュ通知を送信する
まずはデバイストークンを取得します。
デバイストークンについての詳しい解説は割愛しますが、AppDelegateのapplication(_:didRegisterForRemoteNotificationsWithDeviceToken:)
メソッドで取得できるので、てきとうにprint文などを仕込んで取得しましょう。
なお、ここで取得できるデバイストークンはData型なので、以下のメソッドで文字列化しておきます。
let token = deviceToken.map { String(format: "%.2hhx", $0) }.joined()
参考: Swiftでプッシュ通知用のDevice Token(Data型)を16進数文字列へ変換する方法 - Qiita
次にKnuffを起動して、「Choose...」をクリックしてキーチェーンに登録されているプッシュ証明書を選択します。
Token欄にデバイストークンを入力して「Push」をクリックすれば端末にプッシュ通知が送信されます。
Payload欄は自由に変更できます(カーソルが動かなかったりで編集できなそうに見えますが編集できました)。
おわりに
Knuffの使い方についてご紹介しました。
なお、Knuffも2020年5月を最後にコミットがないので、そのうち使えなくなってしまうかもしれません。。
[SBI証券]つみたてNISAからNISAに変更してみた
SBI証券でつみたてNISA口座からNISA口座に変更したときの記録です。
変更手順
ログイン直後の画面の右端にある「変更」ボタンをクリックします。
約款を読んだらチェックをつけて、「書類請求を申し込む」ボタンをクリックします。
SBI証券から変更申し込みを行うための書類が届くので、必要事項を記入して返信します。
書類には名前を書くだけで、返信用封筒も同封されてるのでとっても簡単です。
変更にあたっての注意事項
変更を申し込む画面に以下の注意書きがありました。
10/1から12/31の間にお手続きが完了した場合は、その年の勘定設定は変更されず、翌年分からの変更となります。
また、買付・再投資などにより投資可能枠を利用している場合には、その年の1/1から9/30の間は制度上受付できず不備返却となりますのでご注意ください
私の場合、2021年1月からずっとつみたてNISA口座で積立買付をしていたので、9月まではNISA口座への変更ができませんでした(前述の手順で変更しようとしても、変更申し込みができなかった)。
10月に入ったタイミングで、変更申し込みが可能になりました。
10月に変更申し込みをしてすでに変更手続きは完了していますが、注意書きにある通り2021年いっぱいはつみたてNISA口座のままであるため、NISA預りでの買付はできませんでした。
また、10月から12月31日までの間に変更申し込みを行った場合、翌年の1月いっぱいはNISAでの投資可能枠の利用が停止されるとのことです。
NISA預かりでの買付は2022年2月から可能になるということですね。
おわりに
つみたてNISAからNISAに変更する手順と注意事項についてご紹介しました。
なお、NISAからつみたてNISAに変更するときも、手順と注意事項は基本的に同じだったと思います。
本記事が少しでも参考になれば幸いです。
ScrollView内のビューが画面上に表示されたことを検知する仕組みを実装する
ScrollView内のビューが画面上に表示されたらなにか処理をするという仕組みを実装したので、実装方法をご紹介します。
実装したもの
ScrollView内に「TargetView XX」と表示された青い四角形のビューが縦に並んでいます。
スクロールしてこの「TargetView XX」の全体が画面に完全に表示されたら、コンソールに「TargetView XX is visible!!」と表示されるようになっています。
さらにスクロールして、「TargetView XX」がわずかでも隠れたら、コンソールに「TargetView XX is invisible!!」と表示されます。
コードの解説
実装コード全体はこんな感じです。
いくつか細かい部分に分けて解説していきます。
struct ContentView: View { @State private var detectedIds = Set<Int>() var body: some View { VStack(spacing: 0) { Color.red.frame(height: 4) GeometryReader { geometry in ScrollView { LazyVStack(spacing: 32) { ForEach(0..<100) { targetView(id: $0) } } } .coordinateSpace(name: "scroll") .onScroll( scrollViewHeight: geometry.size.height, onItemBecameVisible: { id in if let viewId = id as? Int, !detectedIds.contains(viewId) { print("TargetView \(viewId) is visible!!") detectedIds.insert(viewId) } }, onItemBecameInvisible: { id in if let viewId = id as? Int, detectedIds.contains(viewId) { print("TargetView \(viewId) is invisible!!") detectedIds.remove(viewId) } } ) } Color.red.frame(height: 4) } .background(Color.white) } private func targetView(id: Int) -> some View { Rectangle() .frame(height: 300) .foregroundColor(.blue) .overlay(Text("TargetView \(id)")) .identifiable(coordinateSpaceName: "scroll", id: id) } } extension View { func identifiable(coordinateSpaceName: AnyHashable, id: AnyHashable) -> some View { modifier(ItemModifier(id: id, coordinateSpaceName: coordinateSpaceName)) } func onScroll( scrollViewHeight: CGFloat, onItemBecameVisible: @escaping (AnyHashable) -> Void, onItemBecameInvisible: @escaping (AnyHashable) -> Void ) -> some View { onPreferenceChange(ItemPreferenceKey.self) { values in values.forEach { let cellOffset = $0.rect.minY let threshold = scrollViewHeight - $0.rect.height let isVisible = cellOffset >= 0 && cellOffset <= threshold if isVisible { onItemBecameVisible($0.id) } else { onItemBecameInvisible($0.id) } } } } } private struct Item: Equatable { let id: AnyHashable let rect: CGRect } private struct ItemPreferenceKey: PreferenceKey { typealias Value = [Item] static var defaultValue: Value = [] static func reduce(value: inout Value, nextValue: () -> Value) { value.append(contentsOf: nextValue()) } } private struct ItemModifier: ViewModifier { let id: AnyHashable let coordinateSpaceName: AnyHashable func body(content: Content) -> some View { content.background( GeometryReader { geometry in Color.clear.preference( key: ItemPreferenceKey.self, value: [Item(id: id, rect: geometry.frame(in: .named(coordinateSpaceName)))] ) } ) } }
ビューが画面に表示されたかどうかを判定するための情報を整理する
例えば下図において、TargetView 2の全体が画面に表示されたことを判定するにはどうすれば良いでしょうか?
やり方はいくつかあると思いますが、私は以下の方法をとりました。
まず、以下の値を取得します。
①ScrollViewの高さ
②TargetViewの高さ
③TargetViewのScrollView内におけるオフセット値
③のオフセット値は、ScrollViewの上端つまり上の赤線からTargetViewの上端までの距離を指します。
そして、TargetViewの全体が画面に表示されるのは、①から②を引いた値と③の値とが一致するとき、あるいはそれより小さいときです。
③ <= ① - ②
さらに、スクロールを進めていくとTargetView 2はやがて見えなくなります。
TargetViewがわずかでも隠れたら、非表示と判定したいです。
③はScrollViewの上端より上に行くとマイナスの値を取るため、TargetViewの全体が画面に表示されている状態は以下の式で表せます。
③ >= 0 && ③ <= ① - ②
各情報を取得する
GeometryReaderを使って①ScrollViewの高さを取得しています。
ここでは上下の赤線の間の領域のサイズをGeometryReaderから取得することができます。
VStack(spacing: 0) { Color.red.frame(height: 4) GeometryReader { geometry in ScrollView { ... Color.red.frame(height: 4) }
次に、②TargetViewの高さと③TargetViewのScrollView内におけるオフセット値を取得する方法を説明します。
これらの値はidentifiable(coordinateSpaceName:id:)
というカスタムModifierで簡単に取得できるようにしています。
Rectangle() .frame(height: 300) .foregroundColor(.blue) .overlay(Text("TargetView \(id)")) .identifiable(coordinateSpaceName: "scroll", id: id)
identifiable(coordinateSpaceName:id:)
の実装の詳細はItemModifierというViewModifierに隠蔽されています。
ItemModifierはビューのbackgroundにGeometryReaderを仕込んでビューのフレームを取得します。
backgroundにGeometryReaderを配置してビューのフレームを取得するという方法はよく知られているようです。
また、ある特定の座標空間におけるフレームを取得したいので、ItemModifierにcoordinateSpaceNameというプロパティを持たせています。
今回の実装ではScrollViewの座標空間を指定していて、これによりビューのScrollView内におけるオフセット値が取得できるようになっています。
private struct ItemModifier: ViewModifier { let id: AnyHashable let coordinateSpaceName: AnyHashable func body(content: Content) -> some View { content.background( GeometryReader { geometry in Color.clear.preference( key: ItemPreferenceKey.self, value: [Item(id: id, rect: geometry.frame(in: .named(coordinateSpaceName)))] ) } ) } }
ビューが画面に表示されたかどうかを判定する
ビューが画面に表示されたかどうかを判定するロジックについては前述の通りです。
ここではそのロジックを実装する方法を説明します。
GeometryReaderで取得したTargetViewのフレームは、onPreferenceChange Modifierで取得することができます。
onPreferenceChange Modifierでフレームを取得しビューが画面に表示されたかどうかを判定するというロジックを、onScroll(scrollViewHeight:onItemBecameVisible:onItemBecameInvisible:)
というカスタムModifierとして実装しました。
func onScroll( scrollViewHeight: CGFloat, onItemBecameVisible: @escaping (AnyHashable) -> Void, onItemBecameInvisible: @escaping (AnyHashable) -> Void ) -> some View { onPreferenceChange(ItemPreferenceKey.self) { values in values.forEach { let cellOffset = $0.rect.minY let threshold = scrollViewHeight - $0.rect.height let isVisible = cellOffset >= 0 && cellOffset < threshold if isVisible { onItemBecameVisible($0.id) } else { onItemBecameInvisible($0.id) } } } }
スクロールするとこの部分のロジックが呼ばれ、isVisible
に前述の式の結果が格納されます。
TargetViewの全体が画面に表示されたらisVisible == true
、一部が画面から見えなくなったらisVisible == false
となり、それぞれのクロージャが実行されます。
このカスタムModifierをScrollViewに対して実装してあげれば、TargetViewが画面に表示されたときに何かをするという処理を簡単に実装することができます。
GeometryReader { geometry in ScrollView { ... } .coordinateSpace(name: "scroll") .onScroll( scrollViewHeight: geometry.size.height, onItemBecameVisible: { id in // TargetViewが画面に表示された }, onItemBecameInvisible: { id in // TargetViewが画面から見えなくなった } ) }
まとめ
ScrollView内のビューが画面上に表示されたらなにか処理をするという仕組みの実装方法についてご紹介しました。
SwiftUIで座標を扱うコードを書くためにはGeometryReaderやPreferenceKeyを駆使する必要がありますが、愚直に書くとかなり読みづらいコードになってしまいます。
今回ご紹介したように、適宜カスタムModifierなどを実装して汎用化することで、ある程度読みやすく、また再利用しやすいコードにすることができます。
少しでも参考になれば幸いです。
[SwiftUI] .alert()の記述箇所を気をつけないと子ビューのアラートが表示されなくなる
親ビューと子ビューそれぞれでアラートを表示する実装がある場合、.alert()
Modifierの記述箇所を気をつけないと、子ビューのアラートが表示されなくなります。
正しく表示されるケースの実装はこちらです。
struct ParentView: View { @State var showAlert = false var body: some View { VStack(spacing: 32) { Button("Show parent view alert") { showAlert = true } .alert(isPresented: $showAlert) { Alert(title: Text("Parent view alert")) } ChildView() } } } struct ChildView: View { @State var showAlert = false var body: some View { Button("Show child view alert") { showAlert = true } .alert(isPresented: $showAlert) { Alert(title: Text("Child view alert")) } } }
「Show child view alert」と表示されているボタンをタップすると、子ビューのアラートが表示されます。
次は子ビューのアラートが表示されないケースです。
sruct ParentView: View { @State var showAlert = false var body: some View { VStack(spacing: 32) { Button("Show parent view alert") { showAlert = true } // .alert(isPresented: $showAlert) { // Alert(title: Text("Parent view alert")) // } ChildView() } // ここに移動 .alert(isPresented: $showAlert) { Alert(title: Text("Parent view alert")) } } } struct ChildView: View { @State var showAlert = false var body: some View { Button("Show child view alert") { showAlert = true } .alert(isPresented: $showAlert) { Alert(title: Text("Child view alert")) } } }
正しく表示されるケースでは.alert()
はビュー階層において同階層に実装されていました。
一方、こちらの実装ではChildViewのコンテナ(VStack)に対して.alert()
が実装され、.alert()
が複数階層に渡って実装されたことになります。
.alert()
を複数の階層で実装してしまうと、下層のアラートが表示されなくなってしまうようです。
The Composable Architecture(TCA)で再利用可能なコンポーネントを作る方法
TCAのサンプルの中に Reusable Favoriting Componentというものがあります。
「何かをFavoritingする」という機能を提供する、汎用的なコンポーネントの実装例です。
Getting Startedのサンプルと比べて若干ReducerやStateの書き方が違うので混乱するんですが、よくよく見てみると基本的な構造はCounterサンプルなどと同じでした。
ちょっと個人的なメモみたいな記事になってしまいますが、要点を整理してみたので参考にしてみてください。
Favoriting Componentの構造
TCAは基本的にState、Action、Environment、Reducerの4つでFeature(機能)を構成し、それをビューと組み合わせます。
このサンプルのREADMEで「再利用可能なコンポーネント」と謳ってますが、Favoriting Componentも何か特別な構造を持つわけではなく、この4つで構成されています。
ただ、ジェネリックな型を扱う関係もあり、Reducerだけはグローバルなインタンスとして宣言するのではなく、関数で生成するようになっています。
そして、この関数の構造が若干複雑に見えて、最初は混乱してました。
extension Reducer { /// Enhances a reducer with favoriting logic. func favorite<ID>( state: WritableKeyPath<State, FavoriteState<ID>>, action: CasePath<Action, FavoriteAction>, environment: @escaping (Environment) -> FavoriteEnvironment<ID> ) -> Reducer where ID: Hashable { .combine( self, Reducer<FavoriteState<ID>, FavoriteAction, FavoriteEnvironment> { state, action, environment in switch action { ... // 省略 } } .pullback(state: state, action: action, environment: environment) ) } }
episodeReducerを見てみると、Reducer.favorite()メソッドはあるReducerにFavoriting機能を追加する、という意図が伝わってきます。
let episodeReducer = Reducer<EpisodeState, EpisodeAction, EpisodeEnvironment>.empty.favorite( state: \.favorite, action: /EpisodeAction.favorite, environment: { FavoriteEnvironment(request: $0.favorite, mainQueue: $0.mainQueue) } )
再利用可能なコンポーネントはこうやって実装するのかあと最初はなんとなく思ってたんですが、実は上記の実装はこう書き換えることができます。
extension Reducer { static func favoriteReducer<ID: Hashable>() -> Reducer<FavoriteState<ID>, FavoriteAction, FavoriteEnvironment<ID>> { .init { state, action, environment in switch action { ... // 省略 } } } } let episodeReducer = Reducer<EpisodeState, EpisodeAction, EpisodeEnvironment>.combine( .empty, .favoriteReducer().pullback( state: \.favorite, action: /EpisodeAction.favorite, environment: { FavoriteEnvironment(request: $0.favorite, mainQueue: $0.mainQueue) } ) )
こうしてみると、Getting Startedのサンプルの実装と同じ感じになりますね。
個人的にはReducer.combineでReducerを組み合わせるという構造の方が理解しやすいなあと思いますが、前述の書き方の方がメリットがあったりするのでしょうか?
ただの書き方の違いなのか、前述の書き方の方が色々良い点があるのかどうか、そこまではまだわかっていません。
Stateの構造
EpisodeStateの構造に着目してみます。
EpisodeStateに対応するビューであるEpisodeViewはこの各行を表現しています。
EpisodeStateの実装はこうです。
struct EpisodeState: Equatable, Identifiable { var alert: AlertState<FavoriteAction>? let id: UUID var isFavorite: Bool let title: String var favorite: FavoriteState<ID> { get { .init(alert: self.alert, id: self.id, isFavorite: self.isFavorite) } set { (self.alert, self.isFavorite) = (newValue.alert, newValue.isFavorite) } } }
FavoriteState型のプロパティをStoredプロパティではなくComputedプロパティで実装しているのですが、これの意図がいまいち読み取れていません。
例えば、EpisodeViewでalertやisFavoriteは参照してないのでEpisodeStateで持つ必要あるのかな?と疑問に思います。
仮に、FavoriteState.isFavoriteの状態に応じてビューを変えたいとなった場合でも、普通にfavorite.isFavoriteを参照すればよいだけです。
struct EpisodeView: View { let store: Store<EpisodeState, EpisodeAction> var body: some View { WithViewStore(self.store) { viewStore in HStack(alignment: .firstTextBaseline) { Text(viewStore.title) // ここを追加してみた .foregroundColor(viewStore.favorite.isFavorite ? .red : .black) Spacer() FavoriteButton( store: self.store.scope(state: \.favorite, action: EpisodeAction.favorite)) } } } }
Computedプロパティを使わないとすると、こんな実装になるかと思います。
EpisodeViewでalertとisFavoriteは参照してないので、プロパティを削除してます。
struct EpisodeState: Equatable, Identifiable { let id: UUID let title: String var favorite: FavoriteState<UUID> init(id: UUID, isFavorite: Bool, title: String) { self.id = id self.title = title self.favorite = .init(id: id, isFavorite: isFavorite) }
イニシャライザ書かないといけないというのはありますが、ビューで参照してないプロパティを持つのも微妙かなと思うので、これで良いんじゃないかなと。
サンプルが単純すぎるだけで、もっと複雑なStateの構造になったらComputedプロパティで実装するメリットが出るんでしょうかね。
おわりに
当初は若干複雑に見えたサンプルでしたが、分解して見るとそれまでのサンプルとそれほど変わらない構造であることがわかりました。
もしかしたら、私が読み解けていない部分が多々あるような気もしますが、、今後自分の関わっているプロジェクトで再利用可能なコンポーネントを実装しようとなったときに、このサンプルの実装の意図がわかるのかもしれません。
ジュニアNISAで運用銘柄を入れ替えることはできる?
2021/09/05 追記
本記事で「非課税で運用している銘柄を売却して、その資金で別の銘柄を非課税枠で購入することはできない」と書いたんですが、実際に試したところ、売却して得た資金をすべて非課税NISA口座での買付に利用できてしまいました。。
簡単に状況と経緯をまとめておくとこんな感じです。
- 7月にTOPIX連動インデックスファンドを83,212円で売却
- 20,000円は総合口座の現金残高等に充当されていた
- 63,212円はジュニアNISA口座の現金残高等に充当されていた
- 9月にS&P500連動インデックスファンドをジュニアNISA口座-NISA預かりで83,212円分買付
- 約定後にポートフォリオを確認すると、ジュニアNISA口座-NISA預かりにて購入した分が追加されている
うーん、、SBIのサイトに記載されている内容と一致しないが、自分の理解が間違っているのだろうか。
まあ目的だった運用銘柄の入れ替えができたので、これはこれで良かったです。
引き続き、外国株式インデックスファンドの方もS&P500連動インデックスファンドに入れ替えしようと思います。
はじめに
子どもの教育資金を少しでも増やすため、ジュニアNISAで投資信託の運用を行っています。
これまではTOPIX連動インデックスファンドと外国株式インデックスファンドを購入してきたのですが、いろんな書籍やブログを読み漁った結果、S&P500連動インデックスファンドに切り替えたい気持ちです。
ジュニアNISAは子どもが18歳になるまでは資金の払い出しができないことは承知しているのですが、運用銘柄の入れ替えができるかどうかはわからなかったので調べてみました。
運用銘柄の入れ替えはできない
結論から言うと、運用銘柄の入れ替えはできません。
iDeCoのスイッチングのような仕組みがあることを期待してたんですが、どうやらそのような仕組みはジュニアNISAには用意されていないようです。
ちなみに、以前スイッチングについてこんな記事を書きました。
ジュニアNISAで保有している銘柄を売却するとどうなるのか?
私はSBI証券を使用しているのでSBI証券での話になりますが、おそらく他の金融機関でも同じなのではないかと思います。
ジュニアNISAで保有している銘柄を売却するとどうなるのでしょうか?
SBI証券の「ジュニアNISA取引のご注意事項」のページに以下の記述があります。
払出し制限期間中、「ジュニアNISA口座-NISA預り」で購入した株式を売却した資金は、課税ジュニアNISA口座([2]及び[3])の買付資金としてご利用いただくことができます。
https://search.sbisec.co.jp/v2/popwin/attention/trading/juniornisa_01.html
つまりこういうことですね。
- ジュニアNISA口座で保有している銘柄を売却し、その資金で別の銘柄を非課税のジュニアNISA口座で買付することはできない
- ジュニアNISA口座にも課税口座(特定預かりと一般預かり)があり、その口座でのみ買付することができる
非課税で運用している銘柄を売却して、その資金で別の銘柄を非課税枠で購入することはできない、つまり入れ替えができないということになります。
銘柄次第では入れ替えしても損にはならないのでは?
自分のNISA口座でも以前、保有していた銘柄を全て売却してS&P500インデックスファンドを購入し直したことがありました。
ジュニアNISAでの運用期間はまだ2年なので、売却しても課税はされません。
そして、売却して得た資金で、課税口座で無分配型のS&P500インデックスファンドを購入し、それをずっと保有し続けるのであれば、課税は一切されないため入れ替えをしても特に損にはならないはずです。
NISAの仕組みの細かいところまでは把握してないので落とし穴があるかもしれませんが、、S&P500インデックスファンドへの切り替えを決行しようと思います!