├── LICENSE ├── Package.swift ├── README.md └── UpdateKit ├── Package.swift └── Sources └── UpdateKit ├── UpdateClasses.swift ├── UpdateKit.swift ├── UpdateNotes.swift ├── UpdateViewExamples.swift ├── UpdateViewHandler.swift └── UpdateViewHandler2.swift /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 TheiPhoneDev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "UpdateKit", 8 | platforms: [ 9 | .iOS(.v15), 10 | .visionOS(.v1) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, making them visible to other packages. 14 | .library( 15 | name: "UpdateKit", 16 | targets: ["UpdateKit"]), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package, defining a module or a test suite. 20 | // Targets can depend on other targets in this package and products from dependencies. 21 | .target( 22 | name: "UpdateKit"), 23 | 24 | ], 25 | swiftLanguageModes: [.v6] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UpdateKit 2 | A simple and lightweight Swift library to better present release notes in iOS, iPadOS and visionOS apps. 3 | 4 | ![Swift](https://img.shields.io/badge/Swift-6.0-orange?logo=swift) 5 | ![iOS](https://img.shields.io/badge/iOS-15.0-blue?logo=apple) 6 | ![iPadOS](https://img.shields.io/badge/iPadOS-15.0-blue?logo=apple) 7 | ![visionOS](https://img.shields.io/badge/visionOS-1.0-blue?logo=apple) 8 | 9 | 10 | 11 | ## Requirements 12 | **UpdateKit** requires iOS 15.0 and visionOS 1.0 13 | 14 | ## What is update kit ? 15 | 16 | **UpdateKit** is a simple and lightweight Swift library that simplifies update screens. As a developer I find myself to have to write release notes, and sometime I don't show it in the best way possible to the users. One-time update screen are the best way to show our users the new version's changes and let them know what's new and what's been fixed. 17 | 18 | ## UpdateKit structure 19 | **UpdateKit** is inspired to Apple update screens, which can be seen in iOS built-in apps. I wanted to create a simple structure that is flexible and also customizable and can be suited to the developer's needs. 20 | 21 | ## Features ✨ 22 | - Customizable cells 23 | - Native UI 24 | - Integration with iOS, iPadOS and visionOS 25 | - Attach Views to show what changes have been made 26 | - Thread-safe (this means no data race) by using the Sendable protocol 27 | 28 | 29 | ## Usage 30 | To use UpdateKit, first of all: 31 | ```swift 32 | import UpdateKit 33 | ``` 34 | in your project. You can implement it as SPM (Swift Package Manager). 35 | - Go to the UpdateNotes.swift file and edit the updateNotes collection. That's where the release notes are collected. 36 | - Call the UpdateViewHandler view wherever you need it. 37 | 38 | ## Implementation 39 | ```swift 40 | UpdateViewHandler(updateNotes: notes) 41 | ``` 42 | 43 | ## Screenshots 44 | 45 | 46 | 47 | ## Demo 1 48 | https://github.com/user-attachments/assets/e6bee750-fa39-4261-96d2-f767abf7bd62 49 | 50 | ## Demo 2 51 | https://github.com/user-attachments/assets/f6d3ccf5-e8ee-41dc-b0d2-04dcba1206ec 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /UpdateKit/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "UpdateKit", 8 | platforms: [ 9 | .iOS(.v15), 10 | .visionOS(.v1) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, making them visible to other packages. 14 | .library( 15 | name: "UpdateKit", 16 | targets: ["UpdateKit"]), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package, defining a module or a test suite. 20 | // Targets can depend on other targets in this package and products from dependencies. 21 | .target( 22 | name: "UpdateKit"), 23 | 24 | ], 25 | swiftLanguageModes: [.v6] 26 | ) 27 | -------------------------------------------------------------------------------- /UpdateKit/Sources/UpdateKit/UpdateClasses.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateClasses.swift 3 | // UpdateKit 4 | // 5 | // Created by Pietro Gambatesa on 11/1/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | 12 | 13 | public class Typography: ObservableObject { 14 | @Published public var sfSymbolFontType: Font = .title.weight(.semibold) 15 | @Published public var textTitleFontType: Font = .title2.weight(.semibold) 16 | @Published public var textDescriptionFontType: Font = .subheadline 17 | @Published public var textDescriptionFontColor: Color = .secondary 18 | @Published public var buttonColor: UIColor = .systemBlue 19 | @Published public var imageColor: UIColor = .systemGray 20 | @Published public var imageWidth: CGFloat = 40 21 | @Published public var imageHeight: CGFloat = 40 22 | 23 | public init() {} 24 | 25 | } 26 | 27 | public class UpdatePresenter: ObservableObject { 28 | @AppStorage("isPresentable") public var isPresentable: Bool = true 29 | public init() {} 30 | 31 | @MainActor 32 | public func setTransparentNavBar() { 33 | let appearance = UINavigationBarAppearance() 34 | appearance.configureWithTransparentBackground() 35 | 36 | UINavigationBar.appearance().scrollEdgeAppearance = appearance 37 | } 38 | 39 | @MainActor 40 | public func resetNavBarAppearance() { 41 | let appearance = UINavigationBarAppearance() 42 | appearance.configureWithDefaultBackground() 43 | 44 | UINavigationBar.appearance().standardAppearance = appearance 45 | UINavigationBar.appearance().scrollEdgeAppearance = appearance 46 | } 47 | 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /UpdateKit/Sources/UpdateKit/UpdateKit.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | -------------------------------------------------------------------------------- /UpdateKit/Sources/UpdateKit/UpdateNotes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateNotes.swift 3 | // UpdateKit 4 | // 5 | // Created by Pietro Gambatesa on 10/9/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | 12 | public struct UpdateNotes: Identifiable, Sendable { 13 | public var id = UUID() 14 | public var updateNoteTitle: String 15 | public var updateNoteDescription: String 16 | public var updateNoteImageType: String 17 | public var updateNoteImage: String 18 | 19 | public init(updateNoteImageType: String, updateNoteImage: String, updateNoteTitle: String, updateNoteDescription: String) { 20 | self.updateNoteImageType = updateNoteImageType 21 | self.updateNoteImage = updateNoteImage 22 | self.updateNoteTitle = updateNoteTitle 23 | self.updateNoteDescription = updateNoteDescription 24 | } 25 | } 26 | 27 | 28 | public let notes: [UpdateNotes] = [ 29 | .init(updateNoteImageType: "Symbol", updateNoteImage: "sparkles", updateNoteTitle: "New features ✨", updateNoteDescription: "New features are available for you to use."), 30 | .init(updateNoteImageType: "Text", updateNoteImage: "🎨", updateNoteTitle: "New app icons 📱", updateNoteDescription: "New app icons are available for you to use."), 31 | .init(updateNoteImageType: "Symbol", updateNoteImage: "ant.circle.fill", updateNoteTitle: "Bug fixes", updateNoteDescription: "Bug fixes to make the app even faster"), 32 | .init(updateNoteImageType: "Text", updateNoteImage: "🌈", updateNoteTitle: "UI improvements 🌈", updateNoteDescription: "UI improvements to make the app even faster and deliver a better experience"), 33 | ] 34 | 35 | 36 | public struct UpdateNotesWithView: Identifiable, Sendable { 37 | public var id = UUID() 38 | public var updateNoteTitle: String 39 | public var updateNoteDescription: String 40 | public var updateNoteImageType: String 41 | public var updateNoteImage: String 42 | public var updateView: AnySendableView 43 | public var hasConnectedView: Bool = false 44 | 45 | public init(updateNoteImageType: String, updateNoteImage: String, updateNoteTitle: String, updateNoteDescription: String, updateView: AnySendableView, hasConnectedView: Bool) { 46 | self.updateNoteImageType = updateNoteImageType 47 | self.updateNoteImage = updateNoteImage 48 | self.updateNoteTitle = updateNoteTitle 49 | self.updateNoteDescription = updateNoteDescription 50 | self.updateView = updateView 51 | self.hasConnectedView = hasConnectedView 52 | } 53 | } 54 | 55 | @MainActor 56 | public let viewnotes: [UpdateNotesWithView] = [ 57 | .init(updateNoteImageType: "Symbol", updateNoteImage: "sparkles", updateNoteTitle: "New features ✨", updateNoteDescription: "New features are available for you to use.", updateView: AnySendableView(TestView1()), hasConnectedView: true), 58 | .init(updateNoteImageType: "Text", updateNoteImage: "🎨", updateNoteTitle: "New app icons 📱", updateNoteDescription: "New app icons are available for you to use.", updateView: AnySendableView(TestView2()), hasConnectedView: true), 59 | .init(updateNoteImageType: "Symbol", updateNoteImage: "ant.circle.fill", updateNoteTitle: "Bug fixes", updateNoteDescription: "Bug fixes to make the app even faster", updateView: AnySendableView(TestView3()), hasConnectedView: true), 60 | .init(updateNoteImageType: "Text", updateNoteImage: "🌈", updateNoteTitle: "UI improvements 🌈", updateNoteDescription: "UI improvements to make the app even faster and deliver a better experience", updateView: AnySendableView(TestView1()), hasConnectedView: false), 61 | ] 62 | 63 | 64 | //Wrapper to use View with the Sendable type 65 | public struct AnySendableView: View, Sendable { 66 | private let viewBuilder: () -> AnyView 67 | 68 | // Initialize with any View 69 | public init(_ view: V) { 70 | self.viewBuilder = { AnyView(view) } 71 | } 72 | 73 | // Conforming to View 74 | public var body: some View { 75 | viewBuilder() 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /UpdateKit/Sources/UpdateKit/UpdateViewExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateViewExamples.swift 3 | // UpdateKit 4 | // 5 | // Created by Pietro Gambatesa on 11/1/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct TestView1: View { 12 | var body: some View { 13 | Text("Hello View 1") 14 | } 15 | } 16 | 17 | struct TestView2: View { 18 | var body: some View { 19 | Text("Hello View 2") 20 | } 21 | } 22 | 23 | struct TestView3: View { 24 | var body: some View { 25 | Text("Hello View 3") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /UpdateKit/Sources/UpdateKit/UpdateViewHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateViewHandler.swift 3 | // UpdateKit 4 | // 5 | // Created by Pietro Gambatesa on 10/9/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct UpdateViewHandler: View { 11 | 12 | public var updateNotes: [UpdateNotes] = [] 13 | @StateObject var typography = Typography() 14 | @StateObject var updatePresenter = UpdatePresenter() 15 | @Environment(\.presentationMode) var presentationMode 16 | 17 | public init(updateNotes: [UpdateNotes] = []) { 18 | self.updateNotes = updateNotes 19 | } 20 | 21 | public var body: some View { 22 | NavigationView(content: { 23 | ZStack { 24 | VStack { 25 | ScrollView(.vertical, showsIndicators: false, content: { 26 | VStack(spacing: 15) { 27 | ForEach(updateNotes) { note in 28 | VStack { 29 | HStack(spacing: 20) { 30 | if note.updateNoteImageType == "Image" { 31 | Image(note.updateNoteImage) 32 | .resizable() 33 | .frame(width: typography.imageWidth, height: typography.imageHeight) 34 | .cornerRadius(10) 35 | } else if note.updateNoteImageType == "Symbol" { 36 | Image(systemName: note.updateNoteImage) 37 | .font(typography.sfSymbolFontType) 38 | .foregroundColor(Color(uiColor: typography.imageColor)) 39 | 40 | } else if note.updateNoteImageType == "Text" { 41 | Text(note.updateNoteImage) 42 | .font(typography.sfSymbolFontType) 43 | } 44 | VStack(alignment: .leading) { 45 | Text(note.updateNoteTitle) 46 | .font(typography.textTitleFontType) 47 | Text(note.updateNoteDescription) 48 | .font(typography.textDescriptionFontType) 49 | .foregroundColor(typography.textDescriptionFontColor) 50 | 51 | } 52 | }.padding() 53 | .frame(maxWidth: .infinity, alignment: .leading) 54 | } 55 | 56 | } 57 | } 58 | 59 | }) 60 | #if os(iOS) 61 | Button { 62 | self.presentationMode.wrappedValue.dismiss() 63 | self.updatePresenter.isPresentable = false 64 | } label: { 65 | Text("Continue") 66 | .font(.title3.weight(.medium)) 67 | .padding(.leading,60) 68 | .padding(.trailing,60) 69 | .padding(.bottom,10) 70 | .padding(.top,10) 71 | }.buttonStyle(.borderedProminent) 72 | .tint(Color(uiColor: typography.buttonColor)) 73 | .padding(.bottom,10) 74 | #endif 75 | #if os(visionOS) 76 | Button { 77 | self.presentationMode.wrappedValue.dismiss() 78 | self.updatePresenter.isPresentable = false 79 | } label: { 80 | Text("Continue") 81 | .font(.title3.weight(.medium)) 82 | .padding(.leading,60) 83 | .padding(.trailing,60) 84 | .padding(.bottom,10) 85 | .padding(.top,10) 86 | }.buttonStyle(.borderedProminent) 87 | .padding(.bottom,20) 88 | #endif 89 | 90 | } 91 | }.toolbar(content: { 92 | ToolbarItem(placement: .principal, content: { 93 | Text("What's new") 94 | .font(.title3.weight(.semibold)) 95 | .frame(maxWidth: .infinity, alignment: .center) 96 | }) 97 | }) 98 | }).navigationViewStyle(.stack) 99 | } 100 | } 101 | 102 | 103 | 104 | 105 | #Preview { 106 | UpdateViewHandler(updateNotes: notes) 107 | } 108 | 109 | -------------------------------------------------------------------------------- /UpdateKit/Sources/UpdateKit/UpdateViewHandler2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateViewHandler2.swift 3 | // UpdateKit 4 | // 5 | // Created by Pietro Gambatesa on 11/1/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct UpdateViewHandler2: View { 11 | 12 | public var viewNotes: [UpdateNotesWithView] = [] 13 | @StateObject var typography = Typography() 14 | @StateObject var updatePresenter = UpdatePresenter() 15 | @Environment(\.presentationMode) var presentationMode 16 | 17 | public init(viewNotes: [UpdateNotesWithView] = []) { 18 | self.viewNotes = viewNotes 19 | } 20 | 21 | public var body: some View { 22 | NavigationView(content: { 23 | ZStack { 24 | VStack { 25 | ScrollView(.vertical, showsIndicators: false, content: { 26 | VStack(spacing: 15) { 27 | ForEach(viewNotes) { note in 28 | VStack { 29 | HStack(spacing: 20) { 30 | if note.updateNoteImageType == "Image" { 31 | Image(note.updateNoteImage) 32 | .resizable() 33 | .frame(width: typography.imageWidth, height: typography.imageHeight) 34 | .cornerRadius(10) 35 | } else if note.updateNoteImageType == "Symbol" { 36 | Image(systemName: note.updateNoteImage) 37 | .font(typography.sfSymbolFontType) 38 | .foregroundColor(Color(uiColor: typography.imageColor)) 39 | 40 | } else if note.updateNoteImageType == "Text" { 41 | Text(note.updateNoteImage) 42 | .font(typography.sfSymbolFontType) 43 | } 44 | VStack(alignment: .leading) { 45 | Text(note.updateNoteTitle) 46 | .font(typography.textTitleFontType) 47 | Text(note.updateNoteDescription) 48 | .font(typography.textDescriptionFontType) 49 | .foregroundColor(typography.textDescriptionFontColor) 50 | if note.hasConnectedView == true { 51 | NavigationLink { 52 | note.updateView 53 | } label: { 54 | Text("See more") 55 | }.frame(maxWidth: .infinity, alignment: .trailing) 56 | } 57 | } 58 | }.padding() 59 | .frame(maxWidth: .infinity, alignment: .leading) 60 | .padding(.trailing, 20) 61 | } 62 | 63 | } 64 | } 65 | 66 | }) 67 | #if os(iOS) 68 | Button { 69 | self.presentationMode.wrappedValue.dismiss() 70 | self.updatePresenter.isPresentable = false 71 | } label: { 72 | Text("Continue") 73 | .font(.title3.weight(.medium)) 74 | .padding(.leading,60) 75 | .padding(.trailing,60) 76 | .padding(.bottom,10) 77 | .padding(.top,10) 78 | }.buttonStyle(.borderedProminent) 79 | .tint(Color(uiColor: typography.buttonColor)) 80 | .padding(.bottom,10) 81 | #endif 82 | #if os(visionOS) 83 | Button { 84 | self.presentationMode.wrappedValue.dismiss() 85 | self.updatePresenter.isPresentable = false 86 | } label: { 87 | Text("Continue") 88 | .font(.title3.weight(.medium)) 89 | .padding(.leading,60) 90 | .padding(.trailing,60) 91 | .padding(.bottom,10) 92 | .padding(.top,10) 93 | }.buttonStyle(.borderedProminent) 94 | .padding(.bottom,20) 95 | #endif 96 | 97 | } 98 | }.toolbar(content: { 99 | ToolbarItem(placement: .principal, content: { 100 | Text("What's new") 101 | .font(.title3.weight(.semibold)) 102 | .frame(maxWidth: .infinity, alignment: .center) 103 | }) 104 | }) 105 | }).navigationViewStyle(.stack) 106 | } 107 | } 108 | 109 | 110 | 111 | #Preview { 112 | UpdateViewHandler2(viewNotes: viewnotes) 113 | } 114 | --------------------------------------------------------------------------------