├── .gitignore ├── Documents └── Readme.png ├── NewsApp ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Utils │ ├── Constants.swift │ └── URLHelper.swift ├── NewsAppApp.swift ├── Models │ ├── NewsSource.swift │ └── NewsArticle.swift ├── View Models │ ├── NewsSourceListViewModel.swift │ └── NewsArticleListViewModel.swift ├── Services │ └── Webservice.swift └── Views │ ├── NewsListScreen.swift │ └── NewsSourceListScreen.swift ├── NewsApp.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ ├── azamsharp.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ ├── aaronfononi.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ └── yasinerdemli.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ ├── azamsharp.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ ├── aaronfononi.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ └── yasinerdemli.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Documents/Readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AranFononi/NewsApp-SwiftUI-Concurrency/HEAD/Documents/Readme.png -------------------------------------------------------------------------------- /NewsApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NewsApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NewsApp/Utils/Constants.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | struct Constants { 5 | #error("Please enter your API Key from newsapi.org, and then delete this.") 6 | static let apiKey = "YOURAPIKEY" 7 | } 8 | -------------------------------------------------------------------------------- /NewsApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NewsApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /NewsApp/NewsAppApp.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import SwiftUI 4 | 5 | @main 6 | struct NewsAppApp: App { 7 | var body: some Scene { 8 | WindowGroup { 9 | NewsSourceListScreen() 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NewsApp.xcodeproj/project.xcworkspace/xcuserdata/azamsharp.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AranFononi/NewsApp-SwiftUI-Concurrency/HEAD/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/azamsharp.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /NewsApp.xcodeproj/project.xcworkspace/xcuserdata/aaronfononi.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AranFononi/NewsApp-SwiftUI-Concurrency/HEAD/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/aaronfononi.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /NewsApp.xcodeproj/project.xcworkspace/xcuserdata/yasinerdemli.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AranFononi/NewsApp-SwiftUI-Concurrency/HEAD/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/yasinerdemli.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /NewsApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NewsApp/Models/NewsSource.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Foundation 4 | 5 | typealias NewsSources = [NewsSource] 6 | 7 | struct NewsSourceResponse: Decodable { 8 | let sources: NewsSources 9 | } 10 | 11 | struct NewsSource: Decodable, Identifiable, Hashable { 12 | let id: String 13 | let name: String 14 | let description: String 15 | 16 | static let example = NewsSource(id: "abc-news", name: "ABC News", description: "This is ABC news") 17 | } 18 | -------------------------------------------------------------------------------- /NewsApp.xcodeproj/xcuserdata/azamsharp.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | NewsApp.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /NewsApp.xcodeproj/xcuserdata/aaronfononi.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | NewsApp.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /NewsApp.xcodeproj/xcuserdata/yasinerdemli.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | NewsApp.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /NewsApp/View Models/NewsSourceListViewModel.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Foundation 4 | @MainActor 5 | class NewsSourceListViewModel: ObservableObject { 6 | @Published var newsSources: NewsSources = [] 7 | let service = Webservice() 8 | 9 | func getSources() async { 10 | do { 11 | let newsSources = try await service.fetchSources() 12 | self.newsSources = newsSources 13 | } catch { 14 | print(error) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /NewsApp/View Models/NewsArticleListViewModel.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Foundation 4 | @MainActor 5 | class NewsArticleListViewModel: ObservableObject { 6 | @Published var newsArticles: NewsArticles = [] 7 | let service = Webservice() 8 | 9 | func getNewsBy(sourceId: String) async { 10 | do { 11 | let newsArticles = try await service.fetchNews(by: sourceId) 12 | self.newsArticles = newsArticles 13 | } catch { 14 | print(error) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /NewsApp/Services/Webservice.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | actor Webservice { 5 | let decoder = JSONDecoder() 6 | func fetchSources() async throws -> NewsSources { 7 | let url = try URLHelper.sources() 8 | let (data, _) = try await URLSession.shared.data(from: url) 9 | let newsSourceResponse = try decoder.decode(NewsSourceResponse.self, from: data) 10 | 11 | return newsSourceResponse.sources 12 | } 13 | 14 | func fetchNews(by sourceID: String) async throws -> NewsArticles { 15 | let url = try URLHelper.topHeadlines(source: sourceID) 16 | let (data, _) = try await URLSession.shared.data(from: url) 17 | let newsArticlesResponse = try decoder.decode(NewsArticleResponse.self, from: data) 18 | return newsArticlesResponse.articles 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NewsApp/Utils/URLHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLHelper.swift 3 | // NewsApp 4 | // 5 | // Created by Yasin Erdemli on 5/3/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct URLHelper { 11 | static private var baseComponents: URLComponents { 12 | var components = URLComponents() 13 | components.scheme = "https" 14 | components.host = "newsapi.org" 15 | return components 16 | } 17 | 18 | static func topHeadlines(source: String) throws -> URL { 19 | var components = baseComponents 20 | components.path = "/v2/top-headlines" 21 | components.queryItems = [ 22 | URLQueryItem(name: "sources", value: source), 23 | URLQueryItem(name: "apiKey", value: Constants.apiKey) 24 | ] 25 | guard let url = components.url else { 26 | throw URLError(.badURL) 27 | } 28 | return url 29 | } 30 | 31 | static func sources() throws -> URL { 32 | var components = baseComponents 33 | components.path = "/v2/sources" 34 | components.queryItems = [URLQueryItem(name: "apiKey", value: Constants.apiKey)] 35 | 36 | guard let url = components.url else { 37 | throw URLError(.badURL) 38 | } 39 | return url 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /NewsApp/Views/NewsListScreen.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import SwiftUI 4 | 5 | struct NewsListScreen: View { 6 | @StateObject private var viewModel = NewsArticleListViewModel() 7 | let newsSource: NewsSource 8 | var body: some View { 9 | List(viewModel.newsArticles) { newsArticle in 10 | NewsArticleCell(newsArticle: newsArticle) 11 | } 12 | .listStyle(.plain) 13 | .task { 14 | await viewModel.getNewsBy(sourceId: newsSource.id) 15 | } 16 | .navigationTitle(newsSource.name) 17 | } 18 | } 19 | 20 | struct NewsListScreen_Previews: PreviewProvider { 21 | static var previews: some View { 22 | NewsListScreen(newsSource: .example) 23 | } 24 | } 25 | 26 | struct NewsArticleCell: View { 27 | let newsArticle: NewsArticle 28 | 29 | var body: some View { 30 | HStack(alignment: .top) { 31 | AsyncImage(url: newsArticle.urlToImage) { image in 32 | image.resizable() 33 | .frame(maxWidth: 100, maxHeight: 100) 34 | } placeholder: { 35 | ProgressView("Loading...") 36 | .frame(maxWidth: 100, maxHeight: 100) 37 | } 38 | 39 | VStack { 40 | Text(newsArticle.title) 41 | .fontWeight(.bold) 42 | Text(newsArticle.description) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /NewsApp/Views/NewsSourceListScreen.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import SwiftUI 4 | 5 | struct NewsSourceListScreen: View { 6 | @StateObject private var viewModel = NewsSourceListViewModel() 7 | var body: some View { 8 | NavigationStack { 9 | List(viewModel.newsSources) { newsSource in 10 | NavigationLink(value: newsSource) { 11 | NewsSourceCell(newsSource: newsSource) 12 | } 13 | } 14 | .listStyle(.plain) 15 | .refreshable { 16 | await viewModel.getSources() 17 | } 18 | .navigationDestination(for: NewsSource.self) { source in 19 | NewsListScreen(newsSource: source) 20 | } 21 | .navigationTitle("News Sources") 22 | .toolbar { 23 | ToolbarItem(placement: .topBarTrailing) { 24 | Button("refresh", systemImage: "arrow.clockwise.circle") { 25 | Task { 26 | await viewModel.getSources() 27 | } 28 | } 29 | } 30 | } 31 | .task { 32 | await viewModel.getSources() 33 | } 34 | } 35 | } 36 | } 37 | 38 | struct NewsSourceListScreen_Previews: PreviewProvider { 39 | static var previews: some View { 40 | NewsSourceListScreen() 41 | } 42 | } 43 | 44 | struct NewsSourceCell: View { 45 | let newsSource: NewsSource 46 | 47 | var body: some View { 48 | VStack(alignment: .leading, spacing: 10) { 49 | Text(newsSource.name) 50 | .font(.headline) 51 | Text(newsSource.description) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /NewsApp/Models/NewsArticle.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | typealias NewsArticles = [NewsArticle] 4 | 5 | struct NewsArticleResponse: Decodable { 6 | let articles: NewsArticles 7 | } 8 | 9 | struct NewsArticle: Decodable, Identifiable { 10 | var id: String { 11 | author + title + publishedAt 12 | } 13 | let author: String 14 | let title: String 15 | let description: String 16 | let url: URL? 17 | let content: String 18 | let publishedAt: String 19 | let urlToImage: URL? 20 | 21 | enum CodingKeys: String, CodingKey { 22 | case author 23 | case title 24 | case description 25 | case url 26 | case content 27 | case publishedAt 28 | case urlToImage 29 | } 30 | 31 | init(from decoder: Decoder) throws { 32 | let container = try decoder.container(keyedBy: CodingKeys.self) 33 | 34 | self.author = (try? container.decode(String.self, forKey: .author)) ?? "" 35 | self.title = try container.decode(String.self, forKey: .title) 36 | self.description = (try? container.decode(String.self, forKey: .description)) ?? "" 37 | if let urlString = try? container.decode(String.self, forKey: .url), let url = URL(string: urlString) { 38 | self.url = url 39 | } else { 40 | self.url = nil 41 | } 42 | self.content = (try? container.decode(String.self, forKey: .content)) ?? "" 43 | self.publishedAt = try container.decode(String.self, forKey: .publishedAt) 44 | 45 | if let urlToImageString = try? container.decode(String.self, forKey: .urlToImage), let urlToImage = URL(string: urlToImageString) { 46 | self.urlToImage = urlToImage 47 | } else { 48 | self.urlToImage = nil 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /NewsApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Async News iOS APP 📰⚡️ 4 | 5 | ### Asynchronous News Fetching with Swift Concurrency 6 | 7 | The **NewsApp** is an iOS project built using SwiftUI and Swift Concurrency (Async/Await, GCD). Originally a course challenge, this project was taken a step further by fully implementing async data fetching, making API calls more efficient, and improving overall responsiveness. The app fetches news from different sources and displays the latest articles in a clean SwiftUI interface. 8 | 9 | ## Project Overview 10 | This project focuses on modern concurrency techniques in Swift, replacing traditional completion handlers with async/await for a more structured and readable approach. The app communicates with the [NewsAPI.org](https://newsapi.org/) service to fetch real-time news updates. 11 | 12 | ⚠️ **Note:** To run this project, replace `YOURAPIKEY` in `Constants.swift` with your own API key from NewsAPI.org. 13 | 14 | ## Learning Outcomes 15 | - **Swift Concurrency**: Implemented async/await to manage API calls efficiently. 16 | - **Grand Central Dispatch (GCD)**: Used `DispatchQueue.main.async` to update the UI correctly. 17 | - **MVVM Architecture**: Applied MVVM principles to separate business logic from UI. 18 | - **SwiftUI Best Practices**: Built a clean and scalable UI using SwiftUI. 19 | 20 | ## Key Skills 21 | - Swift Concurrency (Async/Await) 22 | - Grand Central Dispatch (GCD) 23 | - API Integration & Network Calls 24 | - SwiftUI UI Development 25 | - MVVM Architectural Pattern 26 | 27 | ## Features 28 | ✅ Fetch news sources asynchronously 29 | ✅ Display latest articles with images 30 | ✅ Pull-to-refresh functionality 31 | ✅ Fully async network requests using `URLSession` 32 | 33 | --- 34 | 35 | ### Image Placeholder 36 | ![Placeholder](./Documents/Readme.png) 37 | 38 | --- 39 | 40 | ## Usage 41 | 1. Clone the repository. 42 | 2. Open `Constants.swift` and **replace `YOURAPIKEY` with your NewsAPI.org API key**. 43 | 3. Run the project on Xcode (iOS 16+ recommended). 44 | 45 | --- 46 | 47 | ## Contact 48 | For more information, feel free to reach out: 49 | - **Email**: [aranfononi@gmail.com](mailto:aranfononi@gmail.com) 50 | -------------------------------------------------------------------------------- /NewsApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7DA3F2D52D78595900E724A7 /* URLHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA3F2D42D78595900E724A7 /* URLHelper.swift */; }; 11 | 8D0A08E6268CDCD100CC2EC9 /* NewsAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0A08E5268CDCD100CC2EC9 /* NewsAppApp.swift */; }; 12 | 8D0A08EA268CDCD100CC2EC9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8D0A08E9268CDCD100CC2EC9 /* Assets.xcassets */; }; 13 | 8D0A08ED268CDCD100CC2EC9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8D0A08EC268CDCD100CC2EC9 /* Preview Assets.xcassets */; }; 14 | 8D0A08F8268CEAB000CC2EC9 /* Webservice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0A08F7268CEAB000CC2EC9 /* Webservice.swift */; }; 15 | 8D0A08FA268CEAEC00CC2EC9 /* NewsArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0A08F9268CEAEC00CC2EC9 /* NewsArticle.swift */; }; 16 | 8D0A08FC268CEC8600CC2EC9 /* NewsArticleListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0A08FB268CEC8600CC2EC9 /* NewsArticleListViewModel.swift */; }; 17 | 8D0A08FE268CED2000CC2EC9 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0A08FD268CED2000CC2EC9 /* Constants.swift */; }; 18 | 8D0A0900268CF64D00CC2EC9 /* NewsListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0A08FF268CF64D00CC2EC9 /* NewsListScreen.swift */; }; 19 | 8D0A0905268CFC4B00CC2EC9 /* NewsSourceListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0A0904268CFC4B00CC2EC9 /* NewsSourceListScreen.swift */; }; 20 | 8D0A0907268CFC7700CC2EC9 /* NewsSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0A0906268CFC7700CC2EC9 /* NewsSource.swift */; }; 21 | 8D0A0909268CFD6700CC2EC9 /* NewsSourceListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0A0908268CFD6700CC2EC9 /* NewsSourceListViewModel.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 7DA3F2D42D78595900E724A7 /* URLHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLHelper.swift; sourceTree = ""; }; 26 | 8D0A08E2268CDCD100CC2EC9 /* NewsApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NewsApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 8D0A08E5268CDCD100CC2EC9 /* NewsAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsAppApp.swift; sourceTree = ""; }; 28 | 8D0A08E9268CDCD100CC2EC9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 29 | 8D0A08EC268CDCD100CC2EC9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 30 | 8D0A08F7268CEAB000CC2EC9 /* Webservice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Webservice.swift; sourceTree = ""; }; 31 | 8D0A08F9268CEAEC00CC2EC9 /* NewsArticle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsArticle.swift; sourceTree = ""; }; 32 | 8D0A08FB268CEC8600CC2EC9 /* NewsArticleListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsArticleListViewModel.swift; sourceTree = ""; }; 33 | 8D0A08FD268CED2000CC2EC9 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 34 | 8D0A08FF268CF64D00CC2EC9 /* NewsListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsListScreen.swift; sourceTree = ""; }; 35 | 8D0A0904268CFC4B00CC2EC9 /* NewsSourceListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsSourceListScreen.swift; sourceTree = ""; }; 36 | 8D0A0906268CFC7700CC2EC9 /* NewsSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsSource.swift; sourceTree = ""; }; 37 | 8D0A0908268CFD6700CC2EC9 /* NewsSourceListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsSourceListViewModel.swift; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | 8D0A08DF268CDCD100CC2EC9 /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXFrameworksBuildPhase section */ 49 | 50 | /* Begin PBXGroup section */ 51 | 8D0A08D9268CDCD100CC2EC9 = { 52 | isa = PBXGroup; 53 | children = ( 54 | 8D0A08E4268CDCD100CC2EC9 /* NewsApp */, 55 | 8D0A08E3268CDCD100CC2EC9 /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | 8D0A08E3268CDCD100CC2EC9 /* Products */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 8D0A08E2268CDCD100CC2EC9 /* NewsApp.app */, 63 | ); 64 | name = Products; 65 | sourceTree = ""; 66 | }; 67 | 8D0A08E4268CDCD100CC2EC9 /* NewsApp */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | 8D0A0901268CFBC900CC2EC9 /* Views */, 71 | 8D0A08F6268CEAA300CC2EC9 /* Services */, 72 | 8D0A08F5268CEA9D00CC2EC9 /* Utils */, 73 | 8D0A08F4268CEA9800CC2EC9 /* Models */, 74 | 8D0A08F3268CEA8B00CC2EC9 /* View Models */, 75 | 8D0A08E5268CDCD100CC2EC9 /* NewsAppApp.swift */, 76 | 8D0A08E9268CDCD100CC2EC9 /* Assets.xcassets */, 77 | 8D0A08EB268CDCD100CC2EC9 /* Preview Content */, 78 | ); 79 | path = NewsApp; 80 | sourceTree = ""; 81 | }; 82 | 8D0A08EB268CDCD100CC2EC9 /* Preview Content */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 8D0A08EC268CDCD100CC2EC9 /* Preview Assets.xcassets */, 86 | ); 87 | path = "Preview Content"; 88 | sourceTree = ""; 89 | }; 90 | 8D0A08F3268CEA8B00CC2EC9 /* View Models */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 8D0A08FB268CEC8600CC2EC9 /* NewsArticleListViewModel.swift */, 94 | 8D0A0908268CFD6700CC2EC9 /* NewsSourceListViewModel.swift */, 95 | ); 96 | path = "View Models"; 97 | sourceTree = ""; 98 | }; 99 | 8D0A08F4268CEA9800CC2EC9 /* Models */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 8D0A08F9268CEAEC00CC2EC9 /* NewsArticle.swift */, 103 | 8D0A0906268CFC7700CC2EC9 /* NewsSource.swift */, 104 | ); 105 | path = Models; 106 | sourceTree = ""; 107 | }; 108 | 8D0A08F5268CEA9D00CC2EC9 /* Utils */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 8D0A08FD268CED2000CC2EC9 /* Constants.swift */, 112 | 7DA3F2D42D78595900E724A7 /* URLHelper.swift */, 113 | ); 114 | path = Utils; 115 | sourceTree = ""; 116 | }; 117 | 8D0A08F6268CEAA300CC2EC9 /* Services */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 8D0A08F7268CEAB000CC2EC9 /* Webservice.swift */, 121 | ); 122 | path = Services; 123 | sourceTree = ""; 124 | }; 125 | 8D0A0901268CFBC900CC2EC9 /* Views */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 8D0A08FF268CF64D00CC2EC9 /* NewsListScreen.swift */, 129 | 8D0A0904268CFC4B00CC2EC9 /* NewsSourceListScreen.swift */, 130 | ); 131 | path = Views; 132 | sourceTree = ""; 133 | }; 134 | /* End PBXGroup section */ 135 | 136 | /* Begin PBXNativeTarget section */ 137 | 8D0A08E1268CDCD100CC2EC9 /* NewsApp */ = { 138 | isa = PBXNativeTarget; 139 | buildConfigurationList = 8D0A08F0268CDCD100CC2EC9 /* Build configuration list for PBXNativeTarget "NewsApp" */; 140 | buildPhases = ( 141 | 8D0A08DE268CDCD100CC2EC9 /* Sources */, 142 | 8D0A08DF268CDCD100CC2EC9 /* Frameworks */, 143 | 8D0A08E0268CDCD100CC2EC9 /* Resources */, 144 | ); 145 | buildRules = ( 146 | ); 147 | dependencies = ( 148 | ); 149 | name = NewsApp; 150 | productName = NewsApp; 151 | productReference = 8D0A08E2268CDCD100CC2EC9 /* NewsApp.app */; 152 | productType = "com.apple.product-type.application"; 153 | }; 154 | /* End PBXNativeTarget section */ 155 | 156 | /* Begin PBXProject section */ 157 | 8D0A08DA268CDCD100CC2EC9 /* Project object */ = { 158 | isa = PBXProject; 159 | attributes = { 160 | BuildIndependentTargetsInParallel = 1; 161 | LastSwiftUpdateCheck = 1300; 162 | LastUpgradeCheck = 1620; 163 | TargetAttributes = { 164 | 8D0A08E1268CDCD100CC2EC9 = { 165 | CreatedOnToolsVersion = 13.0; 166 | }; 167 | }; 168 | }; 169 | buildConfigurationList = 8D0A08DD268CDCD100CC2EC9 /* Build configuration list for PBXProject "NewsApp" */; 170 | compatibilityVersion = "Xcode 13.0"; 171 | developmentRegion = en; 172 | hasScannedForEncodings = 0; 173 | knownRegions = ( 174 | en, 175 | Base, 176 | ); 177 | mainGroup = 8D0A08D9268CDCD100CC2EC9; 178 | productRefGroup = 8D0A08E3268CDCD100CC2EC9 /* Products */; 179 | projectDirPath = ""; 180 | projectRoot = ""; 181 | targets = ( 182 | 8D0A08E1268CDCD100CC2EC9 /* NewsApp */, 183 | ); 184 | }; 185 | /* End PBXProject section */ 186 | 187 | /* Begin PBXResourcesBuildPhase section */ 188 | 8D0A08E0268CDCD100CC2EC9 /* Resources */ = { 189 | isa = PBXResourcesBuildPhase; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | 8D0A08ED268CDCD100CC2EC9 /* Preview Assets.xcassets in Resources */, 193 | 8D0A08EA268CDCD100CC2EC9 /* Assets.xcassets in Resources */, 194 | ); 195 | runOnlyForDeploymentPostprocessing = 0; 196 | }; 197 | /* End PBXResourcesBuildPhase section */ 198 | 199 | /* Begin PBXSourcesBuildPhase section */ 200 | 8D0A08DE268CDCD100CC2EC9 /* Sources */ = { 201 | isa = PBXSourcesBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | 8D0A08FC268CEC8600CC2EC9 /* NewsArticleListViewModel.swift in Sources */, 205 | 8D0A0907268CFC7700CC2EC9 /* NewsSource.swift in Sources */, 206 | 8D0A08FA268CEAEC00CC2EC9 /* NewsArticle.swift in Sources */, 207 | 8D0A0909268CFD6700CC2EC9 /* NewsSourceListViewModel.swift in Sources */, 208 | 8D0A08E6268CDCD100CC2EC9 /* NewsAppApp.swift in Sources */, 209 | 7DA3F2D52D78595900E724A7 /* URLHelper.swift in Sources */, 210 | 8D0A0905268CFC4B00CC2EC9 /* NewsSourceListScreen.swift in Sources */, 211 | 8D0A08F8268CEAB000CC2EC9 /* Webservice.swift in Sources */, 212 | 8D0A0900268CF64D00CC2EC9 /* NewsListScreen.swift in Sources */, 213 | 8D0A08FE268CED2000CC2EC9 /* Constants.swift in Sources */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | /* End PBXSourcesBuildPhase section */ 218 | 219 | /* Begin XCBuildConfiguration section */ 220 | 8D0A08EE268CDCD100CC2EC9 /* Debug */ = { 221 | isa = XCBuildConfiguration; 222 | buildSettings = { 223 | ALWAYS_SEARCH_USER_PATHS = NO; 224 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 225 | CLANG_ANALYZER_NONNULL = YES; 226 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 227 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 228 | CLANG_CXX_LIBRARY = "libc++"; 229 | CLANG_ENABLE_MODULES = YES; 230 | CLANG_ENABLE_OBJC_ARC = YES; 231 | CLANG_ENABLE_OBJC_WEAK = YES; 232 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 233 | CLANG_WARN_BOOL_CONVERSION = YES; 234 | CLANG_WARN_COMMA = YES; 235 | CLANG_WARN_CONSTANT_CONVERSION = YES; 236 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 237 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 238 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 239 | CLANG_WARN_EMPTY_BODY = YES; 240 | CLANG_WARN_ENUM_CONVERSION = YES; 241 | CLANG_WARN_INFINITE_RECURSION = YES; 242 | CLANG_WARN_INT_CONVERSION = YES; 243 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 244 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 245 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 246 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 247 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 248 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 249 | CLANG_WARN_STRICT_PROTOTYPES = YES; 250 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 251 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 252 | CLANG_WARN_UNREACHABLE_CODE = YES; 253 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 254 | COPY_PHASE_STRIP = NO; 255 | DEBUG_INFORMATION_FORMAT = dwarf; 256 | ENABLE_STRICT_OBJC_MSGSEND = YES; 257 | ENABLE_TESTABILITY = YES; 258 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 259 | GCC_C_LANGUAGE_STANDARD = gnu11; 260 | GCC_DYNAMIC_NO_PIC = NO; 261 | GCC_NO_COMMON_BLOCKS = YES; 262 | GCC_OPTIMIZATION_LEVEL = 0; 263 | GCC_PREPROCESSOR_DEFINITIONS = ( 264 | "DEBUG=1", 265 | "$(inherited)", 266 | ); 267 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 268 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 269 | GCC_WARN_UNDECLARED_SELECTOR = YES; 270 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 271 | GCC_WARN_UNUSED_FUNCTION = YES; 272 | GCC_WARN_UNUSED_VARIABLE = YES; 273 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 274 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 275 | MTL_FAST_MATH = YES; 276 | ONLY_ACTIVE_ARCH = YES; 277 | SDKROOT = iphoneos; 278 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 279 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 280 | }; 281 | name = Debug; 282 | }; 283 | 8D0A08EF268CDCD100CC2EC9 /* Release */ = { 284 | isa = XCBuildConfiguration; 285 | buildSettings = { 286 | ALWAYS_SEARCH_USER_PATHS = NO; 287 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 288 | CLANG_ANALYZER_NONNULL = YES; 289 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 290 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 291 | CLANG_CXX_LIBRARY = "libc++"; 292 | CLANG_ENABLE_MODULES = YES; 293 | CLANG_ENABLE_OBJC_ARC = YES; 294 | CLANG_ENABLE_OBJC_WEAK = YES; 295 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 296 | CLANG_WARN_BOOL_CONVERSION = YES; 297 | CLANG_WARN_COMMA = YES; 298 | CLANG_WARN_CONSTANT_CONVERSION = YES; 299 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 300 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 301 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 302 | CLANG_WARN_EMPTY_BODY = YES; 303 | CLANG_WARN_ENUM_CONVERSION = YES; 304 | CLANG_WARN_INFINITE_RECURSION = YES; 305 | CLANG_WARN_INT_CONVERSION = YES; 306 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 307 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 308 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 309 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 310 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 311 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 312 | CLANG_WARN_STRICT_PROTOTYPES = YES; 313 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 314 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 315 | CLANG_WARN_UNREACHABLE_CODE = YES; 316 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 317 | COPY_PHASE_STRIP = NO; 318 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 319 | ENABLE_NS_ASSERTIONS = NO; 320 | ENABLE_STRICT_OBJC_MSGSEND = YES; 321 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 322 | GCC_C_LANGUAGE_STANDARD = gnu11; 323 | GCC_NO_COMMON_BLOCKS = YES; 324 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 325 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 326 | GCC_WARN_UNDECLARED_SELECTOR = YES; 327 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 328 | GCC_WARN_UNUSED_FUNCTION = YES; 329 | GCC_WARN_UNUSED_VARIABLE = YES; 330 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 331 | MTL_ENABLE_DEBUG_INFO = NO; 332 | MTL_FAST_MATH = YES; 333 | SDKROOT = iphoneos; 334 | SWIFT_COMPILATION_MODE = wholemodule; 335 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 336 | VALIDATE_PRODUCT = YES; 337 | }; 338 | name = Release; 339 | }; 340 | 8D0A08F1268CDCD100CC2EC9 /* Debug */ = { 341 | isa = XCBuildConfiguration; 342 | buildSettings = { 343 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 344 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 345 | CODE_SIGN_STYLE = Automatic; 346 | CURRENT_PROJECT_VERSION = 1; 347 | DEVELOPMENT_ASSET_PATHS = "\"NewsApp/Preview Content\""; 348 | DEVELOPMENT_TEAM = ""; 349 | ENABLE_PREVIEWS = YES; 350 | GENERATE_INFOPLIST_FILE = YES; 351 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 352 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 353 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 354 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 355 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 356 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 357 | LD_RUNPATH_SEARCH_PATHS = ( 358 | "$(inherited)", 359 | "@executable_path/Frameworks", 360 | ); 361 | MARKETING_VERSION = 1.0; 362 | PRODUCT_BUNDLE_IDENTIFIER = com.aranapps.NewsApp; 363 | PRODUCT_NAME = "$(TARGET_NAME)"; 364 | SWIFT_EMIT_LOC_STRINGS = YES; 365 | SWIFT_VERSION = 5.0; 366 | TARGETED_DEVICE_FAMILY = "1,2"; 367 | }; 368 | name = Debug; 369 | }; 370 | 8D0A08F2268CDCD100CC2EC9 /* Release */ = { 371 | isa = XCBuildConfiguration; 372 | buildSettings = { 373 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 374 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 375 | CODE_SIGN_STYLE = Automatic; 376 | CURRENT_PROJECT_VERSION = 1; 377 | DEVELOPMENT_ASSET_PATHS = "\"NewsApp/Preview Content\""; 378 | DEVELOPMENT_TEAM = ""; 379 | ENABLE_PREVIEWS = YES; 380 | GENERATE_INFOPLIST_FILE = YES; 381 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 382 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 383 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 384 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 385 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 386 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 387 | LD_RUNPATH_SEARCH_PATHS = ( 388 | "$(inherited)", 389 | "@executable_path/Frameworks", 390 | ); 391 | MARKETING_VERSION = 1.0; 392 | PRODUCT_BUNDLE_IDENTIFIER = com.aranapps.NewsApp; 393 | PRODUCT_NAME = "$(TARGET_NAME)"; 394 | SWIFT_EMIT_LOC_STRINGS = YES; 395 | SWIFT_VERSION = 5.0; 396 | TARGETED_DEVICE_FAMILY = "1,2"; 397 | }; 398 | name = Release; 399 | }; 400 | /* End XCBuildConfiguration section */ 401 | 402 | /* Begin XCConfigurationList section */ 403 | 8D0A08DD268CDCD100CC2EC9 /* Build configuration list for PBXProject "NewsApp" */ = { 404 | isa = XCConfigurationList; 405 | buildConfigurations = ( 406 | 8D0A08EE268CDCD100CC2EC9 /* Debug */, 407 | 8D0A08EF268CDCD100CC2EC9 /* Release */, 408 | ); 409 | defaultConfigurationIsVisible = 0; 410 | defaultConfigurationName = Release; 411 | }; 412 | 8D0A08F0268CDCD100CC2EC9 /* Build configuration list for PBXNativeTarget "NewsApp" */ = { 413 | isa = XCConfigurationList; 414 | buildConfigurations = ( 415 | 8D0A08F1268CDCD100CC2EC9 /* Debug */, 416 | 8D0A08F2268CDCD100CC2EC9 /* Release */, 417 | ); 418 | defaultConfigurationIsVisible = 0; 419 | defaultConfigurationName = Release; 420 | }; 421 | /* End XCConfigurationList section */ 422 | }; 423 | rootObject = 8D0A08DA268CDCD100CC2EC9 /* Project object */; 424 | } 425 | --------------------------------------------------------------------------------