-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
nobody asked, yet here they are: iOS 14 widgets!
- Loading branch information
Showing
32 changed files
with
2,398 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
// | ||
// AppsWidget.swift | ||
// WidgetsExtension | ||
// | ||
// Created by ned on 08/03/21. | ||
// Copyright © 2021 ned. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import WidgetKit | ||
import SwiftUI | ||
import Intents | ||
import Combine | ||
import Localize_Swift | ||
|
||
private var cancellables = Set<AnyCancellable>() | ||
|
||
struct AppsWidgetsTimelineEntry: TimelineEntry { | ||
let date: Date | ||
let configuration: ConfigurationIntent | ||
let content: AppdbSearchResource.Response? | ||
} | ||
|
||
struct AppsWidgetsProvider: IntentTimelineProvider { | ||
|
||
typealias Entry = AppsWidgetsTimelineEntry | ||
|
||
let appdbRepository = AppdbRepository() | ||
|
||
func placeholder(in context: Context) -> AppsWidgetsTimelineEntry { | ||
Entry(date: Date(), configuration: ConfigurationIntent(), content: nil) | ||
} | ||
|
||
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Entry) -> Void) { | ||
completion(Entry(date: Date(), configuration: configuration, content: nil)) | ||
} | ||
|
||
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> Void) { | ||
|
||
appdbRepository.fetchAPIResource(AppdbSearchResource(configuration.type, configuration.order, configuration.price)) | ||
.receive(on: RunLoop.main) | ||
.sink(receiveCompletion: { | ||
switch $0 { | ||
case .failure(let error): | ||
print(error.localizedDescription) | ||
completion(Timeline(entries: [], policy: .atEnd)) | ||
default: break | ||
} | ||
}, receiveValue: { | ||
let entries = [ | ||
Entry(date: Date(), configuration: configuration, content: $0) | ||
] | ||
let nextUpdate = Calendar.autoupdatingCurrent.date(byAdding: .hour, value: 6, to: Calendar.autoupdatingCurrent.startOfDay(for: Date()))! | ||
let timeline = Timeline(entries: entries, policy: .after(nextUpdate)) | ||
completion(timeline) | ||
}) | ||
.store(in: &cancellables) | ||
} | ||
} | ||
|
||
struct AppsWidgetsEntryView: View { | ||
|
||
var entry: AppsWidgetsProvider.Entry | ||
|
||
var orderString: String { | ||
switch entry.configuration.order { | ||
case .recent: return "Recently Uploaded".localized() | ||
case .today: return "Popular Today".localized() | ||
case .week: return "Popular This Week".localized() | ||
case .month: return "Popular This Month".localized() | ||
case .year: return "Popular This Year".localized() | ||
case .all_time: return "Popular All Time".localized() | ||
case .unknown: return "" | ||
} | ||
} | ||
|
||
var typeString: String { | ||
switch entry.configuration.type { | ||
case .ios: return "iOS".localized() | ||
case .cydia: return "Cydia".localized() | ||
case .books: return "Books".localized() | ||
case .unknown: return "" | ||
} | ||
} | ||
|
||
var body: some View { | ||
if entry.content == nil || entry.content!.data.isEmpty { | ||
let dummyData = [Content](repeating: Content.dummy, count: 25) | ||
AppsWidgetsMainContentView(date: entry.date, header: orderString, type: typeString, content: dummyData) | ||
.redacted(reason: .placeholder) | ||
} else { | ||
AppsWidgetsMainContentView(date: entry.date, header: orderString, type: typeString, content: entry.content!.data) | ||
} | ||
} | ||
} | ||
|
||
struct AppsWidgetsMainContentView: View { | ||
|
||
let date: Date | ||
let header: String | ||
let type: String | ||
let content: [Content] | ||
@Environment(\.widgetFamily) var family | ||
|
||
var body: some View { | ||
VStack(spacing: 0) { | ||
AppsWidgetHeader(date: date, header: header, type: type) | ||
.padding(.leading) | ||
.padding(.trailing) | ||
.padding(.top, 10) | ||
.padding(.bottom, 8) | ||
.background(Color("BackgroundColorHeader")) | ||
|
||
Divider() | ||
|
||
if family == .systemSmall { | ||
let slicedData = Array(content.prefix(3)) | ||
VStack(spacing: 6) { | ||
ForEach(0..<slicedData.count) { i in | ||
let app = slicedData[i] | ||
HStack(spacing: 5) { | ||
Text(getMedal(for: i)) | ||
.font(.system(size: 13)) | ||
.padding(.leading, 5) | ||
AppListView(app: app, contentType: type.lowercased()) | ||
} | ||
} | ||
} | ||
.padding(.top, 4) | ||
.padding(.bottom, 8) | ||
} else { | ||
let columns = 5 | ||
let rows = family == .systemLarge ? 5 : 2 | ||
GridStack(rows: rows, columns: columns) { row, col in | ||
if content.indices.contains(row * columns + col) { | ||
let app = content[row * columns + col] | ||
AppGridView(app: app, contentType: type.lowercased()) | ||
} | ||
} | ||
.padding(.leading) | ||
.padding(.trailing) | ||
.padding(.top, 10) | ||
.padding(.bottom, 10) | ||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) | ||
} | ||
} | ||
.background(Color("BackgroundColor")) | ||
} | ||
|
||
func getMedal(for index: Int) -> String { | ||
switch index { | ||
case 0: return "🥇" | ||
case 1: return "🥈" | ||
default: return "🥉" | ||
} | ||
} | ||
} | ||
|
||
struct AppsWidgets: Widget { | ||
|
||
var body: some WidgetConfiguration { | ||
IntentConfiguration(kind: "appdb-apps-widget", intent: ConfigurationIntent.self, provider: AppsWidgetsProvider()) { entry in | ||
AppsWidgetsEntryView(entry: entry) | ||
} | ||
.configurationDisplayName("AppDB content") | ||
.description("Show and configure appdb content") | ||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) | ||
} | ||
} | ||
|
||
struct Apps_Previews: PreviewProvider { | ||
|
||
static var previews: some View { | ||
let dummyData = [Content](repeating: Content.dummy, count: 25) | ||
AppsWidgetsMainContentView(date: Date(), header: "", type: "cydia", content: dummyData) | ||
.previewContext(WidgetPreviewContext(family: .systemSmall)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// | ||
// AppGridView.swift | ||
// WidgetsExtension | ||
// | ||
// Created by ned on 09/03/21. | ||
// Copyright © 2021 ned. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
struct AppGridView: View { | ||
|
||
let app: Content | ||
let contentType: String | ||
|
||
var body: some View { | ||
|
||
let redirectUrl = "appdb-ios://?trackid=\(app.id)&type=\(contentType)" | ||
|
||
VStack { | ||
Link(destination: URL(string: redirectUrl)!) { | ||
RemoteImage(urlString: app.image) | ||
.animation(.easeInOut(duration: 0.25)) | ||
.scaledToFit() | ||
.clipShape(AppIconShape(rounded: contentType != "books")) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// | ||
// AppListView.swift | ||
// WidgetsExtension | ||
// | ||
// Created by ned on 09/03/21. | ||
// Copyright © 2021 ned. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
struct AppListView: View { | ||
let app: Content | ||
let contentType: String | ||
|
||
var body: some View { | ||
HStack { | ||
RemoteImage(urlString: app.image) | ||
.animation(.easeInOut(duration: 0.25)) | ||
.scaledToFit() | ||
.clipShape(AppIconShape(rounded: contentType != "books")) | ||
|
||
Text(app.name) | ||
.font(.system(size: 12)) | ||
.lineLimit(2) | ||
.padding(.trailing, 10) | ||
.fixedSize(horizontal: false, vertical: true) | ||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// | ||
// AppsWidgetHeader.swift | ||
// WidgetsExtension | ||
// | ||
// Created by ned on 09/03/21. | ||
// Copyright © 2021 ned. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
struct AppsWidgetHeader: View { | ||
|
||
let date: Date | ||
let header: String | ||
let type: String | ||
@Environment(\.widgetFamily) var family | ||
|
||
let formatter: DateFormatter = { | ||
let formatter = DateFormatter() | ||
formatter.dateStyle = .none | ||
formatter.timeStyle = .short | ||
return formatter | ||
}() | ||
|
||
var body: some View { | ||
HStack { | ||
HStack(spacing: 7) { | ||
Image("appdb") | ||
.resizable() | ||
.frame(width: 20, height: 20, alignment: .center) | ||
.scaledToFit() | ||
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) | ||
.unredacted() | ||
Text(family == .systemSmall ? "Top 3" : header) | ||
.lineLimit(1) | ||
.font(.system(size: 16, weight: .medium, design: .rounded)) | ||
.fixedSize() | ||
if !properUppercase(type).isEmpty { | ||
RoundedBadge(text: properUppercase(type)) | ||
.fixedSize() | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
} | ||
} | ||
Spacer() | ||
if family != .systemSmall { | ||
Text(formatter.string(from: date)) | ||
.font(.system(size: 13)) | ||
.foregroundColor(.secondary) | ||
} | ||
} | ||
} | ||
|
||
func properUppercase(_ string: String) -> String { | ||
if string == "iOS".localized() { return string } | ||
return string.uppercased() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// | ||
// GridStack.swift | ||
// WidgetsExtension | ||
// | ||
// Created by ned on 09/03/21. | ||
// Copyright © 2021 ned. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
// https://www.hackingwithswift.com/quick-start/swiftui/how-to-position-views-in-a-grid-using-lazyvgrid-and-lazyhgrid | ||
struct GridStack<Content: View>: View { | ||
let rows: Int | ||
let columns: Int | ||
let content: (Int, Int) -> Content | ||
|
||
var body: some View { | ||
VStack { | ||
ForEach(0 ..< rows, id: \.self) { row in | ||
HStack(spacing: 15) { | ||
ForEach(0 ..< columns, id: \.self) { column in | ||
content(row, column) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
init(rows: Int, columns: Int, @ViewBuilder content: @escaping (Int, Int) -> Content) { | ||
self.rows = rows | ||
self.columns = columns | ||
self.content = content | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// | ||
// RemoteImage.swift | ||
// WidgetsExtension | ||
// | ||
// Created by ned on 09/03/21. | ||
// Copyright © 2021 ned. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
// https://github.com/pawello2222/WidgetExamples/blob/main/WidgetExtension/URLImageWidget/URLImageView.swift | ||
struct RemoteImage: View { | ||
|
||
let urlString: String | ||
|
||
@ViewBuilder | ||
var body: some View { | ||
if let url = URL(string: urlString), | ||
let data = try? Data(contentsOf: url), | ||
let uiImage = UIImage(data: data) { | ||
Image(uiImage: uiImage) | ||
.resizable() | ||
} else { | ||
// todo corner | ||
Image("appdb") | ||
.resizable() | ||
.redacted(reason: .placeholder) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// | ||
// RoundedBadge.swift | ||
// WidgetsExtension | ||
// | ||
// Created by ned on 09/03/21. | ||
// Copyright © 2021 ned. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
struct RoundedBadge: View { | ||
|
||
let text: String | ||
|
||
var body: some View { | ||
Text(text) | ||
.font(.system(size: 10, weight: .bold, design: .rounded)) | ||
.kerning(0.5) | ||
.foregroundColor(.white) | ||
.padding(.top, 2) | ||
.padding(.bottom, 2) | ||
.padding(.leading, 6) | ||
.padding(.trailing, 6) | ||
.lineLimit(1) | ||
.background( | ||
RoundedRectangle(cornerRadius: 8, style: .continuous) | ||
.foregroundColor(.accentColor) | ||
) | ||
} | ||
} |
Oops, something went wrong.