Laravelのユニットテストが「Error: Call to a member function connection() on null」エラーになるときの対策

Laravelのユニットテストが「Error: Call to a member function connection() on null」エラーになったので、そのときに行った対策を紹介する。

artisanを使ってテストクラスを作成する。

# php artisan make:test --unit HostTest
Test created successfully.

テストコードを書いて実行する。

vendor/bin/phpunit tests/Unit/HostTest.php 

エラーになった。

Error: Call to a member function connection() on null

エラーになったのは、use句のTestCaseが違うためのようだ。
「use PHPUnit\Framework\TestCase;」を「use Tests\TestCase;」に変更すると、正しく実行できた。

// use PHPUnit\Framework\TestCase;
use Tests\TestCase;

MySQL WorkbenchでSSL connection errorエラー

MySQL WorkbenchからMySQLに接続しようとしたときに、以下のエラーが発生した。

SSL connection error: error:1425F102:SSL routines:ssl_choose_client_version:unsupported protocol

ConnectionのSSLにあるUse SSLをNoに変更して、SSLを無効にすることで解決した。

Jetpack Composeでフルスクリーンで表示するには

Jetpack Composeを使ったAndroidアプリで、全画面表示をする方法です。

環境

  • Kotlin 1.6.10
  • Jetpack Compose 1.1.1

フルスクリーンで表示する

System UI Controller for Jetpack Composeのインストール

フルスクリーン表示には「System UI Controller for Jetpack Compose」を使用します。

build.gradleを編集し以下の行を追加します。

dependencies {
    implementation "com.google.accompanist:accompanist-systemuicontroller:0.23.1"
}

使用するバージョンは、使用するJetpack Composeのバーションによって変わります。

  • Compose 1.0 (1.0.x) → 0.20.3
  • Compose 1.1 (1.1.x) → 0.23.1
  • Compose UI 1.2 (1.2.x) → 0.25.1
  • Compose UI 1.3 (1.3.x) → 0.26.2-beta

詳しくはこちらをご覧ください。

使い方

WindowCompat.setDecorFitsSystemWindows()を使用して、
システムバーの領域にコンテンツが表示されるようにします。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // システムバーの領域にコンテンツが表示されるようにする
        WindowCompat.setDecorFitsSystemWindows(window, false)

システムバーを透明にします。

val systemUiController = rememberSystemUiController()
val useDarkIcons = !isSystemInDarkTheme()

DisposableEffect(systemUiController, useDarkIcons) {
    systemUiController.setSystemBarsColor(
        color = Color.Transparent,
        darkIcons = useDarkIcons
    )

    onDispose {}
}

サンプルプログラム

fillMaxSizeに指定したBoxがフルスクリーンで表示されます。

緑色の領域を画面上のナビゲーションバーや画面下のステータスバーの部分に描画できています。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)
        setContent {
            // Remember a SystemUiController
            val systemUiController = rememberSystemUiController()
            val useDarkIcons = !isSystemInDarkTheme()

            DisposableEffect(systemUiController, useDarkIcons) {
                // すべてのシステム バーの色を透明に更新し、明るいテーマの場合は暗いアイコンを使用します
                systemUiController.setSystemBarsColor(
                    color = Color.Transparent,
                    darkIcons = useDarkIcons
                )
                onDispose {}
            }

            FullScreenSampleTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    MainContent()
                }
            }
        }
    }
}

@Composable
fun MainContent() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.Green)
    ) {
        Text(
            modifier = Modifier.align(alignment = Alignment.TopCenter),
            text = "Top"
        )
        Text(
            modifier = Modifier.align(alignment = Alignment.BottomCenter),
            text = "Bottom"
        )
        Text(
            modifier = Modifier.align(alignment = Alignment.CenterStart),
            text = "Start"
        )
        Text(
            modifier = Modifier.align(alignment = Alignment.CenterEnd),
            text = "End"
        )
    }
}

画面上のナビゲーションバーや画面下のステータスバーの部分を避ける

build.gradleを編集し以下の行を追加します。

dependencies {
    implementation "androidx.compose.foundation:foundation:1.2.1"
}

Modifier.statusBarsPadding()を追加し、ステータスバーのインセットに合わせてパディングを追加します。

modifier = Modifier
    // ステータスバーを避ける
    .statusBarsPadding()

Modifier.navigationBarsPadding()を追加し、ナビゲーションバーのインセットに合わせてパディングを追加します。

modifier = Modifier
    // ナビゲーションバーを避ける
    .navigationBarsPadding()

その他に、
ステータスバーとナビゲーションバーのインセットに合わせてパディングを追加するModifier.systemBarsPadding()、
ディスプレイのカットアウトに合わせてパディングを追加するModifier.displayCutoutPadding()、
などがある。

サンプルプログラム

青色の領域をステータスバーとナビゲーションバーを避けて描画できています。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // システムバーの領域にコンテンツが表示されるようにする
        WindowCompat.setDecorFitsSystemWindows(window, false)
        setContent {
            // Remember a SystemUiController
            val systemUiController = rememberSystemUiController()
            val useDarkIcons = !isSystemInDarkTheme()

            DisposableEffect(systemUiController, useDarkIcons) {
                // すべてのシステム バーの色を透明に更新し、明るいテーマの場合は暗いアイコンを使用します
                systemUiController.setSystemBarsColor(
                    color = Color.Transparent,
                    darkIcons = useDarkIcons
                )
                onDispose {}
            }

            FullScreenSampleTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    MainContent()
                }
            }
        }
    }
}

@Composable
fun MainContent() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.Green)
    ) {
        Box(
            modifier = Modifier
                // ステータスバーを避ける
                .statusBarsPadding()
                // ナビゲーションバーを避ける
                .navigationBarsPadding()
                .fillMaxSize()
                .background(color = Color.Cyan)
        ) {
            Text(
                modifier = Modifier.align(alignment = Alignment.TopCenter),
                text = "Top"
            )
            Text(
                modifier = Modifier.align(alignment = Alignment.BottomCenter),
                text = "Bottom"
            )
            Text(
                modifier = Modifier.align(alignment = Alignment.CenterStart),
                text = "Start"
            )
            Text(
                modifier = Modifier.align(alignment = Alignment.CenterEnd),
                text = "End"
            )
        }
    }
}

OpenAPIを使ってみる(3) iOSアプリ(Swift5)を作成する

OpenAPIを使って、iOSアプリ(Swift)を作成する。

OpenAPIを使ってみる(2) Androidアプリ(Kotlin)を作成する」の続き。

  • 環境
    • macOS Monterey
    • openapi-generator-cli 6.0.1(「brew install openapi-generator」でインストール)

前々回に作成したOpenAPIのAPIドキュメントとテストサーバーを使って、iOSアプリを作成する。

openapi_sampleフォルダーの下にiOSアプリを作成する。

openapi_sample/
    openapi/
        openapi.yml
    ios_app/

OpenAPI Generatorでソースコードを生成する

iOS用のソースコード(Swift5)を生成する。

使用できるオプションを確認する。

openapi-generator config-help -g swift5

openapiフォルダーで以下のコマンドを実行し、iOSのソースコードを生成する。

openapi-generator generate -i openapi.yml -g swift5 -o swift5

openapi/swift5フォルダーにソースコードが生成された。

ライブラリのインストール

CocoaPodsを使って、iOSアプリにライブラリを導入できるようにする。

iOSアプリのプロジェクトフォルダーに移動して、Podfileを生成する。

cd ../ios_app
pod init

Podfileファイルを編集する。

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
platform :ios, '14.0' # Xcodeで指定しているバージョンに合わせる

target 'ios_app' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for ios_app
  pod 'OpenAPIClient', :path => '../openapi/swift5' # 追加

  target 'ios_appTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'ios_appUITests' do
    # Pods for testing
  end

end

ライブラリをインストールする。

pod install

以下のメッセージが表示されたので、Xcodeで設定を変更する。

[!] The `ios_appUITests [Debug]` target overrides the `ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES` build setting defined in `Pods/Target Support Files/Pods-ios_app-ios_appUITests/Pods-ios_app-ios_appUITests.debug.xcconfig'. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.
  1. Xcodeでプロジェクトを選択する。
  2. TARGETSのプロジェクトを選択する。
  3. 「Build Settings」タブを選択し「always」と入力して検索する。
  4. 「Always Embed Swift Standard Libraries」を選択して、Deleteキーで削除する。

もう一度コマンドを実行する。

pod install

正常にインストールできた。

Pod installation complete! There is 1 dependency from the Podfile and 2 total pods installed.

Xcodeのプロジェクトを設定する

「http://〜」にアクセスできるように、info.plistを編集してATSを無効にする。

  1. Xcodeでプロジェクトを選択する。
  2. TARGETSのプロジェクトを選択する。
  3. 「Info」タブを選択し「Custom iOS Target Properties」に追加する。
  4. 「App Transport Security Settings」→「Allow Arbitrary Loads」の値を「YES」にする。

APIを叩く

注意:Xcodeでプロジェクトを開くときは、XXX.xcodeprojでなく、XXX.xcworkspaceを開く。

main関数で初期設定を行う。

import SwiftUI
import OpenAPIClient

@main
struct swift_appApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().onAppear{
                OpenAPIClientAPI.basePath = "http://192.168.10.109:8080/v1"
            }
        }
    }
}

UsersApiのメソッドを呼ぶことでAPIを叩ける。

// ユーザー一覧を取得する
UsersAPI.listUsers() { (users, error) in
}
// ユーザーを取得する
UsersAPI.getUserById(userId: id) { user, error in
}

サンプルプログラム

「Load Users」ボタンを押すとユーザー一覧を取得し、ユーザーをタップすると詳細を表示する。

import SwiftUI
import OpenAPIClient

class ViewModel: ObservableObject {
    @Published var loading:Bool = false
    @Published var users:[SimpleUser] = []
    @Published var user:User? = nil

    func listUsers() {
        self.loading = true
        self.users = []
        DispatchQueue.global(qos: .userInitiated).async {
            sleep(1)
            UsersAPI.listUsers() { (users, error) in
                if let error = error {
                    print(error.localizedDescription.debugDescription)
                    self.loading = false
                    return
                }
                if let users = users {
                    print(users)
                    self.users = users
                }
                self.loading = false
            }
        }
    }

    func getUserById(_ id:Int) {
        loading = true
        user = nil
        DispatchQueue.global(qos: .userInitiated).async {
            sleep(1)
            UsersAPI.getUserById(userId: id) { user, error in
                if let error = error {
                    print(error.localizedDescription.debugDescription)
                    self.loading = false
                    return
                }
                if let user = user {
                    self.user = user
                }
                self.loading = false
            }
        }
    }

    func resetUser() {
        user = nil
    }
}

struct ContentView: View {
    @ObservedObject private var viewModel:ViewModel = ViewModel()

    var body: some View {
        if viewModel.loading {
            VStack() {
                Text("loading")
            }
        } else if (viewModel.user != nil) {
            List{
                Button(
                    action: {viewModel.resetUser()},
                    label: {Text("Close")})
                Text("id: \(viewModel.user!.id!)")
                Text("name: \(viewModel.user!.name!)")
                Text("birthday: \(viewModel.user!.birthday!)")
            }
        } else {
            List {
                Button(
                    action: {viewModel.listUsers()},
                    label: {Text("Load Users")})
                ForEach(viewModel.users, id: \.id) { user in
                    Button(
                        action: {
                            let id:Int = user.id!
                            viewModel.getUserById(id) },
                        label: {
                            Text("id: \(user.id!) name: \(user.name!)")
                        }
                    )
                }
                Spacer()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}