deep-linking-sample/Apps/Wikipedia/Widgets/Widgets/FeaturedArticleWidget.swift
Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

310 lines
7.9 KiB
Swift

import SwiftUI
import WidgetKit
import WMF
import UIKit
// MARK: - Widget
struct FeaturedArticleWidget: Widget {
private let kind: String = WidgetController.SupportedWidget.featuredArticle.identifier
public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: FeaturedArticleProvider(), content: { entry in
FeaturedArticleView(entry: entry)
})
.configurationDisplayName(FeaturedArticleWidget.LocalizedStrings.widgetTitle)
.description(FeaturedArticleWidget.LocalizedStrings.widgetDescription)
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
// MARK: - Timeline Entry
struct FeaturedArticleEntry: TimelineEntry {
// MARK: - Properties
var date: Date
var content: WidgetFeaturedContent?
var fetchError: WidgetContentFetcher.FetcherError?
// MARK: - Computed Properties
var hasDisplayableContent: Bool {
return fetchError == nil && content?.featuredArticle != nil
}
var fetchedLanguageCode: String? {
return content?.featuredArticle?.languageCode
}
var title: String {
return (content?.featuredArticle?.displayTitle as NSString?)?.wmf_stringByRemovingHTML() ?? ""
}
var description: String {
return content?.featuredArticle?.description ?? ""
}
var extract: String {
return content?.featuredArticle?.extract ?? ""
}
var layoutDirection: LayoutDirection {
if let direction = content?.featuredArticle?.languageDirection {
return direction == "rtl" ? .rightToLeft : .leftToRight
}
return .leftToRight
}
var contentURL: URL? {
guard let page = content?.featuredArticle?.contentURL.desktop.page else {
return nil
}
return URL(string: page)
}
var imageData: Data? {
return content?.featuredArticle?.thumbnailImageSource?.data
}
}
// MARK: - Timeline Provider
struct FeaturedArticleProvider: TimelineProvider {
typealias Entry = FeaturedArticleEntry
func placeholder(in context: Context) -> FeaturedArticleEntry {
return FeaturedArticleEntry(date: Date(), content: nil)
}
func getSnapshot(in context: Context, completion: @escaping (FeaturedArticleEntry) -> Void) {
WidgetController.shared.fetchFeaturedContent(isSnapshot: context.isPreview) { result in
let currentDate = Date()
switch result {
case .success(let featuredContent):
completion(FeaturedArticleEntry(date: currentDate, content: featuredContent))
case .failure(let fetchError):
completion(FeaturedArticleEntry(date: currentDate, content: nil, fetchError: fetchError))
}
}
}
func getTimeline(in context: Context, completion: @escaping (Timeline<FeaturedArticleEntry>) -> Void) {
WidgetController.shared.fetchFeaturedContent { result in
let currentDate = Date()
switch result {
case .success(let featuredContent):
completion(Timeline(entries: [FeaturedArticleEntry(date: currentDate, content: featuredContent)], policy: .after(currentDate.randomDateShortlyAfterMidnight() ?? currentDate)))
case .failure(let fetchError):
completion(Timeline(entries: [FeaturedArticleEntry(date: currentDate, content: nil, fetchError: fetchError)], policy: .atEnd))
}
}
}
}
// MARK: - View
struct FeaturedArticleView: View {
@Environment(\.widgetFamily) private var widgetFamily
@Environment(\.colorScheme) private var colorScheme
var entry: FeaturedArticleEntry
var headerCaptionText: String {
switch widgetFamily {
case .systemLarge:
return FeaturedArticleWidget.LocalizedStrings.fromLanguageWikipediaTextFor(languageCode: entry.fetchedLanguageCode)
default:
return FeaturedArticleWidget.LocalizedStrings.widgetTitle
}
}
var headerTitleText: String {
switch widgetFamily {
case .systemLarge:
return FeaturedArticleWidget.LocalizedStrings.widgetTitle
default:
return entry.title
}
}
var backgroundImage: UIImage? {
guard let imageData = entry.imageData else {
return nil
}
return UIImage(data: imageData)
}
// MARK: - Nested Views
@ViewBuilder
var content: some View {
switch widgetFamily {
case .systemLarge:
largeWidgetContent
default:
smallWidgetContent
}
}
var smallWidgetContent: some View {
headerData
.background(Color(colorScheme == .light ? Theme.light.colors.paperBackground : Theme.dark.colors.paperBackground))
}
var largeWidgetContent: some View {
GeometryReader { proxy in
VStack(spacing: 0) {
headerData
.frame(height: proxy.size.height / 2.25)
.clipped()
bodyData
}
}
.background(Color(colorScheme == .light ? Theme.light.colors.paperBackground : Theme.dark.colors.paperBackground))
}
var headerData: some View {
ZStack {
headerBackground
VStack(alignment: .leading, spacing: 4) {
Spacer()
HStack {
Text(headerCaptionText)
.font(.caption2)
.fontWeight(.bold)
.foregroundColor(.white)
.readableShadow(intensity: 0.8)
Spacer()
}
HStack {
Text(headerTitleText)
.font(.headline)
.foregroundColor(.white)
.readableShadow(intensity: 0.8)
Spacer()
}
}
.padding()
.background(
Rectangle()
.foregroundColor(.black)
.mask(LinearGradient(gradient: Gradient(colors: [.clear, .black]), startPoint: .center, endPoint: .bottom))
.opacity(0.35)
)
}
}
var bodyData: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(entry.title)
.foregroundColor(Color(colorScheme == .light ? Theme.light.colors.primaryText : Theme.dark.colors.primaryText))
.font(.custom("Georgia", size: 21, relativeTo: .title))
Spacer()
}
HStack {
Text(entry.description)
.foregroundColor(Color(colorScheme == .light ? Theme.light.colors.secondaryText : Theme.dark.colors.secondaryText))
.font(.caption)
Spacer()
}
Spacer()
.frame(height: 8)
HStack {
Text(entry.extract)
.foregroundColor(Color(colorScheme == .light ? Theme.light.colors.primaryText : Theme.dark.colors.primaryText))
.font(.caption)
.lineLimit(5)
.lineSpacing(4)
.truncationMode(.tail)
}
}
.padding()
}
@ViewBuilder
var headerBackground: some View {
GeometryReader { proxy in
if let backgroundImage = backgroundImage {
Image(uiImage: backgroundImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
ZStack {
Rectangle()
.foregroundColor(Color(UIColor.blue600))
Text(entry.extract)
.font(.headline)
.fontWeight(.semibold)
.lineSpacing(6)
.foregroundColor(Color.black.opacity(0.15))
.frame(width: proxy.size.width * 1.25, height: proxy.size.height * 2, alignment: .topLeading)
.padding(EdgeInsets(top: 16, leading: 10, bottom: 0, trailing: 0))
}
}
}
}
func noContent(message: String) -> some View {
Rectangle()
.foregroundColor(Color(UIColor.gray500))
.overlay(
Text(message)
.font(.caption)
.bold()
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
.foregroundColor(.white)
.padding()
)
}
@ViewBuilder
var widgetBody: some View {
if entry.hasDisplayableContent {
content
.overlay(FeaturedArticleOverlayView())
} else if entry.fetchError == .unsupportedLanguage {
noContent(message: FeaturedArticleWidget.LocalizedStrings.widgetLanguageFailure)
} else {
noContent(message: FeaturedArticleWidget.LocalizedStrings.widgetContentFailure)
}
}
// MARK: - Body
var body: some View {
widgetBody
.widgetURL(entry.contentURL)
.environment(\.layoutDirection, entry.layoutDirection)
.flipsForRightToLeftLayoutDirection(true)
}
}
struct FeaturedArticleOverlayView: View {
var body: some View {
VStack(alignment: .trailing) {
HStack(alignment: .top) {
Spacer()
Image("W")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 16, alignment: .trailing)
.foregroundColor(.white)
.padding(EdgeInsets(top: 16, leading: 0, bottom: 0, trailing: 16))
.readableShadow()
}
Spacer()
.readableShadow()
.padding(EdgeInsets(top: 0, leading: 16, bottom: 16, trailing: 45))
}
.foregroundColor(.white)
}
}