ソースコード source code

下記アプリの主要なソースコードを公開しています。アプリ開発の参考になれば幸いです。

画像等が別途必要ですので下記情報のみでアプリが完成するものではありません。 アプリは少しずつ機能拡張していますのでストア公開されているアプリと内容が異なる場合があります。 コードはコピーして自由にお使いいただけます。ただし著作権は放棄しておりませんので全部の再掲載はご遠慮ください。部分的に再掲載したり、改変して再掲載するのは構いません。 自身のアプリ作成の参考として個人使用・商用問わず自由にお使いいただけます。 コード記述のお手本を示すものではありません。ミニアプリですので変数名などさほど気遣いしていない部分も有りますし間違いも有るかと思いますので参考程度にお考え下さい。 他の賢者の皆様が公開されているコードを参考にした箇所も含まれます。iOSアプリ開発の熟練者が書いたコードではありません。 エンジニア向け技術情報共有サービスではありませんので説明は省いています。ご了承ください。 GitHubなどへの公開は予定しておりません。

下記コードの最終ビルド日: 2022-06-05

ConstValue.swift

//
//  ConstValue.swift
//  OneStroke
//
//  Created by akira ohmachi on 2022/05/31.
//

import SwiftUI

class ConstValue: ObservableObject {

    static let colorBg: Color = Color.init(red:1, green:1, blue:1)
    static let colorStart: Color = Color.init(red:0.274, green:0.27, blue:0.972)
    static let colorSetting: Color = Color.init(red:0.294, green:0.45, blue:0.972)
}

ContentView.swift

//
//  ContentView.swift
//  OneStroke
//
//  Created by akira ohmachi on 2022/06/04.
//

import SwiftUI
import GoogleMobileAds
import UIKit
import AVFoundation

struct ContentView: View {
    @EnvironmentObject var pub: PublicManager
    @State private var destroyFlag: Bool = false
    @State private var webViewQueryString: String = "?dummy"
    @State private var retryCount: Int = 10
    @State private var retryStr: String = NSLocalizedString("retry", comment: "retry") + ":10"
    private var dateFormatter: DateFormatter = DateFormatter()
    private let soundStarts: Array<AVAudioPlayer> = [
        try! AVAudioPlayer(data: NSDataAsset(name: "start")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "start")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "start")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "start")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "start")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "start")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "start")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "start")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "start")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "start")!.data),
    ]
    @State private var soundStartPtr: Int = 0
    private let soundRetrys: Array<AVAudioPlayer> = [
        try! AVAudioPlayer(data: NSDataAsset(name: "retry")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "retry")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "retry")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "retry")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "retry")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "retry")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "retry")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "retry")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "retry")!.data),
        try! AVAudioPlayer(data: NSDataAsset(name: "retry")!.data),
    ]
    @State private var soundRetryPtr: Int = 0
    private func setWebViewQueryString(mode: String) {
        var str: String = "?locale=en"
        str += "&p=" + String(self.pub.peaces)
        str += "&s=" + (self.pub.sound ? "1" : "0")
        if mode == "start" {
            str += "&r=0&a=0"   //default
        } else if mode == "retry" {
            str += "&r=1"
        } else if mode == "answer" {
            str += "&a=1"
        }
        str += "&dateTime=" + dateFormatter.string(from: Date())
        self.webViewQueryString = str
    }
    private func retryCountReduce() {
        self.retryCount -= 1
        if self.retryCount <= 0 {
            self.retryCount = 0
            self.retryStr = NSLocalizedString("retry", comment: "retry")
        } else {
            self.retryStr = NSLocalizedString("retry", comment: "retry") + ":" + String(self.retryCount)
        }
    }
    private func retryCountInitial() {
        self.retryCount = 10
        self.retryStr = NSLocalizedString("retry", comment: "retry") + ":" + String(self.retryCount)
    }
    private func soundPlay(soundId: String) {
        if self.pub.sound {
            if soundId == "start" {
                self.soundStarts[self.soundStartPtr].play()
                self.soundStartPtr += 1
                if self.soundStartPtr >= self.soundStarts.count {
                    self.soundStartPtr = 0
                }
            } else if soundId == "retry" {
                self.soundRetrys[self.soundRetryPtr].play()
                self.soundRetryPtr += 1
                if self.soundRetryPtr >= self.soundRetrys.count {
                    self.soundRetryPtr = 0
                }
            }
        }
    }
    var body: some View {
        NavigationView {
            GeometryReader { bodyView in
                VStack(spacing: 0) {
                    Rectangle().fill(Color.white).frame(width: bodyView.size.width,height: 1)
                    HStack(spacing: 0) {
                        Button(action:{
                            self.setWebViewQueryString(mode: "start")
                            self.retryCountInitial()
                            self.soundPlay(soundId: "start")
                        }){
                            Text("start")
                                .frame(width:bodyView.size.width * 0.3, height: 50, alignment: .center)
                                .background(ConstValue.colorStart)
                                .foregroundColor(Color.white)
                        }
                        Rectangle().fill(Color.white).frame(width: 1,height: 50)
                        Button(action:{
                            self.setWebViewQueryString(mode: "retry")
                            self.retryCountReduce()
                            self.soundPlay(soundId: "retry")
                        }){
                            Text(verbatim: self.retryStr)
                                .frame(width:bodyView.size.width * 0.3, height: 50, alignment: .center)
                                .background(ConstValue.colorStart)
                                .foregroundColor(Color.white)
                        }
                        Rectangle().fill(Color.white).frame(width: 1,height: 50)
                        if self.retryCount == 0 {
                            Button(action:{
                                self.setWebViewQueryString(mode: "answer")
                            }){
                                Text("answer")
                                    .frame(width:bodyView.size.width * 0.2, height: 50, alignment: .center)
                                    .background(ConstValue.colorStart)
                                    .foregroundColor(Color.white)
                            }
                        } else {
                            Text("")
                                .frame(width:bodyView.size.width * 0.2, height: 50, alignment: .center)
                                .background(ConstValue.colorStart)
                                .foregroundColor(Color.white)
                        }
                        Rectangle().fill(Color.white).frame(width: 1,height: 50)
                        VStack(spacing: 0) {
                            NavigationLink(destination: SettingView(),isActive: self.$pub.isSetting) {
                                Button(action:{
                                    self.pub.tmpPeaces = self.pub.peaces
                                    self.pub.tmpSound = self.pub.sound
                                    self.pub.isSetting = true
                                }){
                                    Text("setting")
                                        .frame(width:bodyView.size.width * 0.2, height: 50, alignment: .center)
                                        .background(ConstValue.colorSetting)
                                        .foregroundColor(Color.white)
                                }
                            }
                        }
                    }
                    .frame(width:bodyView.size.width, height:50)
                    Rectangle().fill(Color.white).frame(width: bodyView.size.width,height: 1)
                    ZStack(alignment: .bottom) {
                        VStack {
                            Spacer()
                            VStack {
                                WebView(queryString: self.webViewQueryString)
                            }.frame(width:bodyView.size.width, height:bodyView.size.width)
                            Spacer()
                            Spacer()
                        }
                        HStack(spacing: 0) {
                            Spacer(minLength: 0)
                            PublicManager.AdView().frame(maxWidth: bodyView.size.width, maxHeight: 50)
                            Spacer(minLength: 0)
                        }.background(Color(red:0.2,green:0.2,blue:0.2))
                    }
                }
                .background(ConstValue.colorBg)
                //.navigationBarTitle("")
                .navigationBarHidden(true)
            }
        }
        .navigationViewStyle(StackNavigationViewStyle())
        .onAppear { //アプリ実行時
            self.dateFormatter.dateFormat = "yyyyMMddHHmmss"
            self.setWebViewQueryString(mode: "start")
            self.retryCountInitial()
        }
        .onDisappear {     //アプリ終了時
            self.destroyFlag = true
        }
    }
}

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

PublicManager.swift

//
//  PublicManager.swift
//  OneStroke
//
//  Created by akira ohmachi on 2022/05/31.
//

import SwiftUI
import GoogleMobileAds

class PublicManager: ObservableObject {

    //setting view
    @Published var isSetting: Bool = false

    //--------------------------------------------------

    @AppStorage("peaces") var peaces: Int = 5
    @AppStorage("sound") var sound: Bool = true

    var tmpPeaces: Int = 5
    var tmpSound: Bool = true

    //--------------------------------------------------
    //adMob

    struct AdView: UIViewRepresentable {
        func makeUIView(context: Context) -> GADBannerView {
            let banner = GADBannerView(adSize: GADAdSizeBanner)
            #if DEBUG
            banner.adUnitID = "ca-app-pub-3940256099942544/2934735716"
            #else
            banner.adUnitID = "ca-app-pub-0000000000000000/0000000000"
            #endif
            let scenes = UIApplication.shared.connectedScenes
            let windowScene = scenes.first as? UIWindowScene
            let window = windowScene?.windows.first
            banner.rootViewController = window?.rootViewController
            banner.load(GADRequest())
            return banner
        }

        func updateUIView(_ uiView: GADBannerView, context: Context) {
        }
    }

    //--------------------------------------------------

}

SettingView.swift

//
//  SettingView.swift
//  OneStroke
//
//  Created by akira ohmachi on 2022/05/31.
//

import SwiftUI

struct SettingView: View {
    @EnvironmentObject var pub: PublicManager

    var body: some View {
        VStack() {
            HStack {
                Button(action: {
                    self.pub.isSetting = false
                }) {
                    Image(systemName: "chevron.backward")
                    Text("cancel")
                }
                Spacer()
                Button(action: {
                    self.pub.peaces = self.pub.tmpPeaces
                    self.pub.sound = self.pub.tmpSound
                    self.pub.isSetting = false
                }) {
                    HStack {
                        Text("apply")
                        Image(systemName: "checkmark.circle")
                    }
                }
            }
            .padding(20)
        }
        GeometryReader { bodyView in
            ScrollView {
                 VStack {
                    VStack {
                        self.border()
                        HStack {
                            Text("level")
                                .font(.subheadline)
                            Spacer()
                            Picker(selection: self.$pub.tmpPeaces, label: Text("level")) {
                                ForEach(3 ..< 21, id: \.self) { num in
                                    Text(String(num))
                                }
                            }
                            Text(" ")
                        }.padding(.horizontal)
                        HStack {
                            Text("level1")
                                .font(.footnote)
                            Spacer()
                        }.padding(.horizontal)
                        self.border()
                        Toggle(isOn: self.$pub.tmpSound) {
                            Text("sound")
                                .font(.subheadline)
                        }.padding(.horizontal)
                        HStack {
                            Text("sound1")
                                .font(.footnote)
                            Spacer()
                        }.padding(.horizontal)
                        self.border()
                        VStack {
                            HStack {
                                Text("usage")
                                    .font(.subheadline)
                                Spacer()
                            }
                            HStack {
                                Text("usage1")
                                    .font(.footnote)
                                Spacer()
                            }.padding(.vertical,5)
                        }.padding(.horizontal)
                        self.border()
                    }
                    Spacer(minLength: 150)
                }
            }
        }
        //.navigationBarTitle("Setting",displayMode: .automatic)
        //.navigationViewStyle(StackNavigationViewStyle())
        .navigationBarHidden(true)
        .onTapGesture {     //背景タップでキーボードを閉じる
            UIApplication.shared.closeKeyboard()
        }
    }

    private func border() -> some View {
         var body: some View {
             HStack {
                 Rectangle().fill(Color.init(red: 0.9, green: 0.9, blue: 0.9)).frame(height: 1)
             }
             .padding(.horizontal)
             .padding(.vertical,10)
         }
         return body
    }
}

extension UIApplication {
    func closeKeyboard() {  //キーボードを閉じる
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

struct SettingView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            SettingView().environmentObject(PublicManager())
        }
    }
}

OneStrokeApp.swift

//
//  OneStrokeApp.swift
//  OneStroke
//
//  Created by akira ohmachi on 2022/06/04.
//

import SwiftUI
import GoogleMobileAds

// AppDelegateクラスを定義する
class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

        // Mobile Ads SDKを初期化する
        GADMobileAds.sharedInstance().start(completionHandler: nil)
        
        return true
    }
}

@main
struct OneStrokeApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(PublicManager())
        }
    }
}

WebView.swift


import SwiftUI
import WebKit

extension WKWebView {
    func queryString(_ queryString: String) {
        guard let bundleMainUrl = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "Resource") else { return }
        let fullUrl = URL(string: queryString, relativeTo: bundleMainUrl)
        let request = URLRequest(url: fullUrl!)
        load(request)
    }
}

struct WebView: UIViewRepresentable {
	var queryString: String

	func makeUIView(context: Context) -> WKWebView {
        let configuration = WKWebViewConfiguration()
        configuration.allowsInlineMediaPlayback = true
        configuration.mediaTypesRequiringUserActionForPlayback = []
        return WKWebView(frame: .zero, configuration: configuration)
	}

	func updateUIView(_ uiView: WKWebView, context: Context) {
		uiView.scrollView.bounces = false
		uiView.queryString(queryString)
	}
}