Skip to content

Commit

Permalink
init boilerplate MyApp
Browse files Browse the repository at this point in the history
  • Loading branch information
thomashoangvn committed Dec 3, 2024
1 parent f006735 commit 175c6e6
Show file tree
Hide file tree
Showing 17 changed files with 945 additions and 0 deletions.
564 changes: 564 additions & 0 deletions MyApp.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions MyApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions MyApp/Assets.xcassets/AccentColor.colorset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
35 changes: 35 additions & 0 deletions MyApp/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
6 changes: 6 additions & 0 deletions MyApp/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
18 changes: 18 additions & 0 deletions MyApp/ContentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// ContentView.swift
// MyApp
//
// Created by Thomas on 12/3/24.
//

import SwiftUI

struct ContentView: View {
var body: some View {
UserListView()
}
}

#Preview {
ContentView()
}
15 changes: 15 additions & 0 deletions MyApp/Models/User.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// User.swift
// Boilerplate
//
// Created by Thomas on 12/3/24.
//

// Models/User.swift
import Foundation

struct User: Identifiable {
let id: UUID
let name: String
let age: Int
}
46 changes: 46 additions & 0 deletions MyApp/MyApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// MyApp.swift
// MyApp
//
// Created by Thomas on 12/3/24.
//


import SwiftUI

@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

@Environment(\.scenePhase) var scenePhase

var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .active:
print("App is active")
case .inactive:
print("App is inactive")
case .background:
print("App is in background")
@unknown default:
break
}
}
}
}

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// Notification
UNUserNotificationCenter.current().delegate = self
return true
}
}

extension AppDelegate: UNUserNotificationCenterDelegate {
// Notification
}
6 changes: 6 additions & 0 deletions MyApp/Preview Content/Preview Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
27 changes: 27 additions & 0 deletions MyApp/Services/UserService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// UserService.swift
// Boilerplate
//
// Created by Thomas on 12/3/24.
//

// Services/UserService.swift
import Combine
import Foundation

protocol UserServiceProtocol {
func fetchUsers() -> AnyPublisher<[User], Never>
}

class UserService: UserServiceProtocol {
func fetchUsers() -> AnyPublisher<[User], Never> {
let users = [
User(id: UUID(), name: "Alice", age: 25),
User(id: UUID(), name: "Bob", age: 30),
User(id: UUID(), name: "Charlie", age: 35)
]
return Just(users)
.delay(for: .seconds(1), scheduler: DispatchQueue.main) //Mock delay
.eraseToAnyPublisher()
}
}
31 changes: 31 additions & 0 deletions MyApp/ViewModels/UserViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// UserViewModel.swift
// Boilerplate
//
// Created by Thomas on 12/3/24.
//

// ViewModels/UserViewModel.swift
import Combine
import Foundation

class UserViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading: Bool = false
private var cancellables = Set<AnyCancellable>()
private let userService: UserServiceProtocol

init(userService: UserServiceProtocol = UserService()) {
self.userService = userService
}

func fetchUsers() {
isLoading = true
userService.fetchUsers()
.sink { [weak self] users in
self?.users = users
self?.isLoading = false
}
.store(in: &cancellables)
}
}
23 changes: 23 additions & 0 deletions MyApp/Views/UserDetailView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// UserDetailView.swift
// Boilerplate
//
// Created by Thomas on 12/3/24.
//

// Views/UserDetailView.swift
import SwiftUI

struct UserDetailView: View {
let user: User

var body: some View {
VStack {
Text("Name: \(user.name)")
.font(.title)
Text("Age: \(user.age)")
.font(.subheadline)
}
.padding()
}
}
33 changes: 33 additions & 0 deletions MyApp/Views/UserListView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// UserListView.swift
// Boilerplate
//
// Created by Thomas on 12/3/24.
//

// Views/UserListView.swift
import SwiftUI

struct UserListView: View {
@StateObject private var viewModel = UserViewModel()

var body: some View {
NavigationView {
VStack {
if viewModel.isLoading {
ProgressView("Loading...")
} else {
List(viewModel.users) { user in
NavigationLink(destination: UserDetailView(user: user)) {
Text(user.name)
}
}
}
}
.navigationBarTitle(Text("Users"), displayMode: .automatic)
.onAppear {
viewModel.fetchUsers()
}
}
}
}
17 changes: 17 additions & 0 deletions MyAppTests/MyAppTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// MyAppTests.swift
// MyAppTests
//
// Created by Thomas on 12/3/24.
//

import Testing
@testable import MyApp

struct MyAppTests {

@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}

}
43 changes: 43 additions & 0 deletions MyAppUITests/MyAppUITests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// MyAppUITests.swift
// MyAppUITests
//
// Created by Thomas on 12/3/24.
//

import XCTest

final class MyAppUITests: XCTestCase {

override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.

// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false

// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

@MainActor
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()

// Use XCTAssert and related functions to verify your tests produce the correct results.
}

@MainActor
func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
}
33 changes: 33 additions & 0 deletions MyAppUITests/MyAppUITestsLaunchTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// MyAppUITestsLaunchTests.swift
// MyAppUITests
//
// Created by Thomas on 12/3/24.
//

import XCTest

final class MyAppUITestsLaunchTests: XCTestCase {

override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}

override func setUpWithError() throws {
continueAfterFailure = false
}

@MainActor
func testLaunch() throws {
let app = XCUIApplication()
app.launch()

// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app

let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Combine SwiftUI MVVM Dependency Injection

A boilerplate demonstrates how to use combine and MVVM in the SwiftUI app.

# Structure

ProjectRoot/
├── Models/
│ └── User.swift
├── ViewModels/
│ └── UserViewModel.swift
├── Views/
│ ├── UserListView.swift
│ └── UserDetailView.swift
└── Services/
└── UserService.swift

# Build Tools & Version

Xcode 15.3

iOS 15.6+

## Contributing

We appreciate your interest in contributing to 'Combine SwiftUI MVVM Dependency Injection' . Feel free to open issues or submit pull requests.

## License

This project is licensed under the [BSD-4-Clause License](LICENSE).

0 comments on commit 175c6e6

Please sign in to comment.