Skip to content

Commit

Permalink
nobody asked, yet here they are: iOS 14 widgets!
Browse files Browse the repository at this point in the history
  • Loading branch information
nedley committed Mar 10, 2021
1 parent 9c9b788 commit 5cfcdb9
Show file tree
Hide file tree
Showing 32 changed files with 2,398 additions and 3 deletions.
178 changes: 178 additions & 0 deletions Widgets/Apps Widget/AppsWidget.swift
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))
}
}
29 changes: 29 additions & 0 deletions Widgets/Apps Widget/Components/AppGridView.swift
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"))
}
}
}
}
30 changes: 30 additions & 0 deletions Widgets/Apps Widget/Components/AppListView.swift
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)
}
}
}
57 changes: 57 additions & 0 deletions Widgets/Apps Widget/Components/AppsWidgetHeader.swift
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()
}
}
34 changes: 34 additions & 0 deletions Widgets/Apps Widget/Components/GridStack.swift
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
}
}
30 changes: 30 additions & 0 deletions Widgets/Apps Widget/Components/RemoteImage.swift
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)
}
}
}
30 changes: 30 additions & 0 deletions Widgets/Apps Widget/Components/RoundedBadge.swift
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)
)
}
}
Loading

0 comments on commit 5cfcdb9

Please sign in to comment.