message: Removed unnecesary exceptions and made some changes to the implementation/docs

diff:
@@ -20,5 +20,10 @@ enum LocationAccuracy {
/// Location accuracy is optimized for navigation on iOS and matches the
/// [LocationAccuracy.best] on Android
- bestForNavigation
+ bestForNavigation,
+
+ /// Location accuracy is reduced for iOS 14+ devices, matches the
+ /// [LocationAccuracy.low] and [LocationAccuracy.lowest] on Android and
+ /// matches [LocationAccuracy.lowest] on devices using iOS 13 or below.
+ reduced
}

diff:
-/// An exception thrown when users using iOS 13 or below try to execute the
-/// 'requestTemporaryFullAccuracy' method.
-///
-/// Since Approximate location only supports iOS 14 or above, the exception is
-/// only thrown when using iOS 13 or below.
-class ApproximateLocationNotSupportedException implements Exception {
- /// Constructs the [ApproximateLocationNotSupportedException]
- const ApproximateLocationNotSupportedException(this.message);
-
- /// A [message] describing more details about the exception
- final String? message;
-
- @override
- String toString() {
- if (message == null || message == '') {
- return 'The requestTemporaryFullAccuracy method only supports iOS 14'
- ' or above.';
- }
- return message!;
- }
-}

diff:
export 'activity_missing_exception.dart';
export 'already_subscribed_exception.dart';
-export 'approximate_location_not_supported_exception.dart';
export 'invalid_permission_exception.dart';
export 'location_service_disabled_exception.dart';
export 'permission_definitions_not_found_exception.dart';
export 'permission_denied_exception.dart';
export 'permission_request_in_progress_exception.dart';
export 'position_update_exception.dart';
\ No newline at end of file
-export 'precise_accuracy_enabled_exception.dart';

diff:
-/// An exception thrown when the user already enabled Precise Location fetching.
-///
-/// This exception is thrown only when using iOS 14 or above.
-class PreciseAccuracyEnabledException implements Exception {
- /// Constructs the [PreciseAccuracyEnabledException]
- const PreciseAccuracyEnabledException(this.message);
-
- /// A [message] describing more details about the exception.
- final String? message;
-
- @override
- String toString() {
- if (message == null || message == '') {
- return 'The user already enabled Precise location fetching, when using '
- 'the requestTemporaryFullAccuracy, make sure to check whether the '
- 'user has already given permission to use Precise Accuracy.';
- }
-
- return message!;
- }
-}

diff:
import 'dart:async';
import 'dart:math';
+import 'package:geolocator_platform_interface/geolocator_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:vector_math/vector_math.dart';
@@ -161,15 +162,15 @@ abstract class GeolocatorPlatform extends PlatformInterface {
/// Asks the user for Temporary Precise location access (iOS 14 or above).
///
- /// Throws a [AccuracyDictionaryNotFoundException] when the key
+ /// Returns [LocationAccuracyStatus.precise] if the user already gave
+ /// permission to use Precise Accuracy. If the user uses iOS 13 or below,
+ /// [LocationAccuracyStatus.precise] will also be returned. On other platforms
+ /// an PlatformException will be thrown.
+ ///
+ /// Throws a [PermissionDefinitionsNotFoundException] when the key
/// `NSLocationTemporaryUsageDescriptionDictionary` has not been set in the
/// `Infop.list`.
- /// Throws a [PreciseAccuracyEnabledException] when the user already gave
- /// permission to use Precise Accuracy location fetching.
- /// Throws a [ApproximateLocationNotSupportedException] when Approximate
- /// Location is not supported (iOS 13 or below).
- Future<void> requestTemporaryFullAccuracy() {
- // ignore: lines_longer_than_80_chars
+ Future<LocationAccuracyStatus> requestTemporaryFullAccuracy() async {
throw UnimplementedError(
'requestTemporaryFullAccuracy() has not been implemented');
}

diff:
@@ -226,10 +226,11 @@ class MethodChannelGeolocator extends GeolocatorPlatform {
}
@override
- Future<void> requestTemporaryFullAccuracy() async {
+ Future<LocationAccuracyStatus> requestTemporaryFullAccuracy() async {
try {
- await _methodChannel.invokeMethod<void>('requestTemporaryFullAccuracy');
- return;
+ final int status = await _methodChannel
+ .invokeMethod('requestTemporaryFullAccuracy');
+ return LocationAccuracyStatus.values[status];
} on PlatformException catch (e) {
_handlePlatformException(e);
rethrow;
@@ -262,10 +263,6 @@ class MethodChannelGeolocator extends GeolocatorPlatform {
throw PermissionRequestInProgressException(exception.message);
case 'LOCATION_UPDATE_FAILURE':
throw PositionUpdateException(exception.message);
- case 'PRECISE_ACCURACY_ENABLED':
- throw PreciseAccuracyEnabledException(exception.message);
- case 'APPROXIMATE_LOCATION_NOT_SUPPORTED':
- throw ApproximateLocationNotSupportedException(exception.message);
default:
throw exception;
}

diff:
@@ -134,121 +134,40 @@ void main() {
});
});
- group(
- // ignore: lines_longer_than_80_chars
- 'requestTemporaryFullAccuracy: When requesting Temporary Precise location',
- () {
- test('Should receive an exception when precise location is enabled',
- () async {
+ group('requestTemporaryFullAccuracy: When requesting temporary full'
+ 'accuracy.', () {
+ test('Should receive reduced accuracy if Location Accuracy is pinned to'
+ ' reduced', () async {
+ // Arrange
MethodChannelMock(
channelName: 'flutter.baseflow.com/geolocator',
method: 'requestTemporaryFullAccuracy',
- result: PlatformException(
- code: 'PRECISE_ACCURACY_ENABLED',
- message: 'Precise location is enabled.',
- details: null,
- ),
+ result: 0
);
// Act
- final accuracyFuture =
- MethodChannelGeolocator().requestTemporaryFullAccuracy();
+ final accuracy = await MethodChannelGeolocator()
+ .requestTemporaryFullAccuracy();
// Assert
- expect(
- accuracyFuture,
- throwsA(
- isA<PreciseAccuracyEnabledException>().having(
- (e) => e.toString(),
- 'description',
- 'Precise location is enabled.',
- ),
- ),
- );
+ expect(accuracy, LocationAccuracyStatus.reduced);
});
- test('Should receive an exception when iOS 13 or below is used',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/geolocator',
- method: 'requestTemporaryFullAccuracy',
- result: PlatformException(
- code: 'APPROXIMATE_LOCATION_NOT_SUPPORTED',
- message: 'Approximate location not supported.',
- details: null,
- ),
- );
- // Act
- final accuracyFuture =
- MethodChannelGeolocator().requestTemporaryFullAccuracy();
-
- // Assert
- expect(
- accuracyFuture,
- throwsA(
- isA<ApproximateLocationNotSupportedException>().having(
- (e) => e.toString(),
- 'description',
- 'Approximate location not supported.',
- ),
- ),
- );
- });
- test('Should receive an exception when precise location is enabled',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/geolocator',
- method: 'requestTemporaryFullAccuracy',
- result: PlatformException(
- code: 'PRECISE_ACCURACY_ENABLED',
- message: null,
- details: null,
- ),
- );
-
- // Act
- final accuracyFuture =
- MethodChannelGeolocator().requestTemporaryFullAccuracy();
-
- // Assert
- expect(
- accuracyFuture,
- throwsA(isA<PreciseAccuracyEnabledException>().having(
- (e) => e.toString(),
- '',
- 'The user already enabled Precise location fetching, when using '
- 'the requestTemporaryFullAccuracy, make sure to check '
- 'whether the user has already given permission to use '
- 'Precise Accuracy.')),
- );
- });
- test('Should receive an exception when iOS 13 or below is used',
- () async {
+ test('Should receive reduced accuracy if Location Accuracy is already set'
+ ' to precise location accuracy', () async {
+ // Arrange
MethodChannelMock(
channelName: 'flutter.baseflow.com/geolocator',
method: 'requestTemporaryFullAccuracy',
- result: PlatformException(
- code: 'APPROXIMATE_LOCATION_NOT_SUPPORTED',
- message: null,
- details: null,
- ),
+ result: 1
);
// Act
- final accuracyFuture =
- MethodChannelGeolocator().requestTemporaryFullAccuracy();
+ final accuracy = await MethodChannelGeolocator()
+ .requestTemporaryFullAccuracy();
// Assert
- expect(
- accuracyFuture,
- throwsA(
- isA<ApproximateLocationNotSupportedException>().having(
- (e) => e.toString(),
- '',
- 'The requestTemporaryFullAccuracy method only supports iOS 14'
- ' or above.'),
- ),
- );
+ expect(accuracy, LocationAccuracyStatus.precise);
});
});
@@ -271,6 +190,7 @@ void main() {
expect(locationAccuracy, LocationAccuracyStatus.reduced);
});
+
test('Should receive reduced accuracy if Location Accuracy is reduced',
() async {
// Arrange


message: Format and analyze issues

change_type: MODIFY

diff:
import 'dart:async';
import 'dart:math';
-import 'package:geolocator_platform_interface/geolocator_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:vector_math/vector_math.dart';
+import '../geolocator_platform_interface.dart';
import 'enums/enums.dart';
import 'implementations/method_channel_geolocator.dart';
import 'models/models.dart';

diff:
@@ -228,8 +228,8 @@ class MethodChannelGeolocator extends GeolocatorPlatform {
@override
Future<LocationAccuracyStatus> requestTemporaryFullAccuracy() async {
try {
- final int status = await _methodChannel
- .invokeMethod('requestTemporaryFullAccuracy');
+ final int status =
+ await _methodChannel.invokeMethod('requestTemporaryFullAccuracy');
return LocationAccuracyStatus.values[status];
} on PlatformException catch (e) {
_handlePlatformException(e);

diff:
@@ -134,37 +134,38 @@ void main() {
});
});
- group('requestTemporaryFullAccuracy: When requesting temporary full'
+ group(
+ 'requestTemporaryFullAccuracy: When requesting temporary full'
'accuracy.', () {
- test('Should receive reduced accuracy if Location Accuracy is pinned to'
+ test(
+ 'Should receive reduced accuracy if Location Accuracy is pinned to'
' reduced', () async {
// Arrange
MethodChannelMock(
channelName: 'flutter.baseflow.com/geolocator',
method: 'requestTemporaryFullAccuracy',
- result: 0
- );
+ result: 0);
// Act
- final accuracy = await MethodChannelGeolocator()
- .requestTemporaryFullAccuracy();
+ final accuracy =
+ await MethodChannelGeolocator().requestTemporaryFullAccuracy();
// Assert
expect(accuracy, LocationAccuracyStatus.reduced);
});
- test('Should receive reduced accuracy if Location Accuracy is already set'
+ test(
+ 'Should receive reduced accuracy if Location Accuracy is already set'
' to precise location accuracy', () async {
// Arrange
MethodChannelMock(
channelName: 'flutter.baseflow.com/geolocator',
method: 'requestTemporaryFullAccuracy',
- result: 1
- );
+ result: 1);
// Act
- final accuracy = await MethodChannelGeolocator()
- .requestTemporaryFullAccuracy();
+ final accuracy =
+ await MethodChannelGeolocator().requestTemporaryFullAccuracy();
// Assert
expect(accuracy, LocationAccuracyStatus.precise);
@@ -190,7 +191,6 @@ void main() {
expect(locationAccuracy, LocationAccuracyStatus.reduced);
});
-
test('Should receive reduced accuracy if Location Accuracy is reduced',
() async {
// Arrange

license: MIT License

repo: baseflow/flutter-geolocator

original_message: Format and analyze issues


message: Small correction to documentation

diff:
@@ -23,7 +23,6 @@ enum LocationAccuracy {
bestForNavigation,
/// Location accuracy is reduced for iOS 14+ devices, matches the
- /// [LocationAccuracy.low] and [LocationAccuracy.lowest] on Android and
- /// matches [LocationAccuracy.lowest] on devices using iOS 13 or below.
+ /// [LocationAccuracy.lowest] on iOS 13 and below and all other platforms.
reduced
}

original_message: Small correction to documentation


message: Update geolocator_platform_interface_test.dart

diff:
@@ -75,7 +75,7 @@ void main() {
test(
// ignore: lines_longer_than_80_chars
- 'Default implementation of reqquestTemporaryAccuracy should throw unimplemented error',
+ 'Default implementation of requestTemporaryAccuracy should throw unimplemented error',
() {
// Arrange
final geolocatorPlatform = ExtendsGeolocatorPlatform();


message: Update location_accuracy.dart

diff:
@@ -24,5 +24,5 @@ enum LocationAccuracy {
/// Location accuracy is reduced for iOS 14+ devices, matches the
/// [LocationAccuracy.lowest] on iOS 13 and below and all other platforms.
- reduced
+ reduced,
}

original_message: Update location_accuracy.dart


message: Dependency upgrade

diff:
+## 8.0.3
+
+- Upgraded the geolocator_platform_interface, geolocator_web, geolocator_apple and geolocator_android packages to the latest versions.
+
## 8.0.2
- Updated README.md to clarify the use of platform specific packages.

diff:
name: geolocator
description: Geolocation plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API for generic location (GPS etc.) functions.
-version: 8.0.2
+version: 8.0.3
repository: https://github.com/Baseflow/flutter-geolocator/tree/master/geolocator
issue_tracker: https://github.com/Baseflow/flutter-geolocator/issues?q=is%3Aissue+is%3Aopen
@@ -24,10 +24,10 @@ dependencies:
flutter:
sdk: flutter
- geolocator_platform_interface: ^3.0.1
- geolocator_android: ^3.0.0+1
- geolocator_apple: ^2.0.0+2
- geolocator_web: ^2.1.1
+ geolocator_platform_interface: ^4.0.0
+ geolocator_android: ^3.0.2
+ geolocator_apple: ^2.0.1
+ geolocator_web: ^2.1.3
dev_dependencies:
flutter_test:

original_message: Dependency upgrade (#945)


message:
fix(Villagers): Swipe gesture dismiss the keyboard
* Previously you had to tap "return" if you want to dismiss the
keyboard

diff:
objects = {
/* Begin PBXBuildFile section */
+ 4C382EE8244E418800F446BA /* DismissingKeyboardOnSwipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */; };
68345579244C994C0072FC0C /* StarButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68345578244C994C0072FC0C /* StarButtonView.swift */; };
6834557B244CA8E10072FC0C /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6834557A244CA8E10072FC0C /* URL.swift */; };
687B57602449EEB100C1E86F /* Island.swift in Sources */ = {isa = PBXBuildFile; fileRef = 687B575F2449EEB100C1E86F /* Island.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
+ 4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissingKeyboardOnSwipe.swift; sourceTree = "<group>"; };
68345578244C994C0072FC0C /* StarButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarButtonView.swift; sourceTree = "<group>"; };
6834557A244CA8E10072FC0C /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
687B575F2449EEB100C1E86F /* Island.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Island.swift; sourceTree = "<group>"; };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 4C382EE6244E415800F446BA /* viewModifiers */ = {
+ isa = PBXGroup;
+ children = (
+ 4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */,
+ );
+ path = viewModifiers;
+ sourceTree = "<group>";
+ };
687B5765244A02E400C1E86F /* turnips */ = {
isa = PBXGroup;
children = (
693E0B9D244C872300D20E46 /* environements */,
69AF8C65243E285A001DD9CB /* models */,
69AF8C6B243E2A0C001DD9CB /* viewModels */,
+ 4C382EE6244E415800F446BA /* viewModifiers */,
69AF8C64243E2857001DD9CB /* views */,
69AF8C50243E283D001DD9CB /* AppDelegate.swift */,
69AF8C52243E283D001DD9CB /* SceneDelegate.swift */,
693E0B22244C596D00D20E46 /* ActiveCrittersView.swift in Sources */,
69AF8C51243E283D001DD9CB /* AppDelegate.swift in Sources */,
68BA5834244B9D4100B53811 /* DashboardView.swift in Sources */,
+ 4C382EE8244E418800F446BA /* DismissingKeyboardOnSwipe.swift in Sources */,
697E63F92449DADE008FB710 /* VillagerRowView.swift in Sources */,
697E63EE2449A365008FB710 /* ACNHApiService.swift in Sources */,
693E0B1F244C526A00D20E46 /* ProgressView.swift in Sources */,

diff:
@@ -45,6 +45,7 @@ struct VillagersListView: View {
.background(Color.dialogue)
.navigationBarTitle(Text("Villagers"),
displayMode: .inline)
+ .modifier(DismissingKeyboardOnSwipe())
placeholderView
}
}

license: Apache License 2.0

repo: dimillian/achnbrowserui


diff:
@@ -11,7 +11,11 @@ import SwiftUI
// See https://stackoverflow.com/questions/56491386/how-to-hide-keyboard-when-using-swiftui
struct DismissingKeyboardOnSwipe: ViewModifier {
func body(content: Content) -> some View {
- content.gesture(swipeGesture)
+ #if os(macOS)
+ return content
+ #else
+ return content.gesture(swipeGesture)
+ #endif
}
private var swipeGesture: some Gesture {

original_message:
fix: lists are working as expected after doing a search on macOS

* Previously crashed after doing a search as the ViewModifier
DismissingKeyboardOnSwipe is useless on macOS and call some iOS only
methods under the hood that leads to crash the app.

* Fix #38


message: refactor: improve Combine code for Search Text in List

diff:
@@ -59,17 +59,21 @@ class ItemsViewModel: ObservableObject {
public init(categorie: Categories) {
self.categorie = categorie
- searchCancellable = _searchText
- .projectedValue
+ searchCancellable = $searchText
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
- .sink { [weak self] string in
- if !string.isEmpty {
- self?.searchItems = self?.items.filter({ $0.name.lowercased().contains(string.lowercased()) }) ?? []
- }
- }
+ .filter { !$0.isEmpty }
+ .map(items(with:))
+ .sink(receiveValue: { [weak self] in self?.searchItems = $0 })
+
itemCancellable = Items.shared.$categories.sink { [weak self] items in
self?.items = items[categorie] ?? []
}
}
+
+ private func items(with string: String) -> [Item] {
+ items.filter {
+ $0.name.lowercased().contains(string.lowercased()) == true
+ }
+ }
}

diff:
@@ -31,13 +31,12 @@ class VillagersViewModel: ObservableObject {
}
init() {
- searchCancellable = _searchText
- .projectedValue
+ searchCancellable = $searchText
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.map(villagers(with:))
- .assign(to: \.searchResults, on: self)
+ .sink(receiveValue: { [weak self] in self?.searchResults = $0 })
}
func fetch() {


message: fix(Categories): search text works without crashes while quickly adding or removing characters

diff:
@@ -17,25 +17,22 @@ class CategoriesSearchViewModel: ObservableObject {
private var searchCancellable: AnyCancellable?
init() {
- searchCancellable = _searchText
- .projectedValue
+ searchCancellable = $searchText
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.map(itemsInCategories(with:))
- .assign(to: \.searchResults, on: self)
+ .subscribe(on: DispatchQueue.global())
+ .receive(on: DispatchQueue.main)
+ .sink(receiveValue: { [weak self] in self?.searchResults = $0})
}
private func itemsInCategories(with string: String) -> [Categories: [Item]] {
- var results: [Categories: [Item]] = [:]
- Items.shared.categories.forEach {
- let items = $1.filter {
+ Items.shared.categories
+ .mapValues({ $0.filter({
$0.name.lowercased().contains(string.lowercased())
- }.prefix(5).compactMap{ $0 }
- if !items.isEmpty {
- results[$0] = items
- }
- }
- return results
+ }) })
+ .filter { !$0.value.isEmpty }
+ .mapValues { Array($0.prefix(5)) }
}
}

diff:
@@ -83,7 +83,7 @@ struct CategoriesView: View {
}
}
}
- }
+ }.modifier(DismissingKeyboardOnSwipe())
}
.navigationBarTitle(Text("Catalog"), displayMode: .inline)
.background(Color.grassBackground)


message: fix(Categories): avoid crashes by ordering search results

diff:
@@ -67,28 +67,48 @@ struct CategoriesView: View {
.padding(.top, 8)
.padding(.bottom, 4)
.background(Color.grassBackground)
- List {
if viewModel.searchText.isEmpty {
- makeNatureCell()
- makeWardrobeCell()
- makeCategories()
+ categoriesList
} else {
- ForEach(viewModel.searchResults.keys.map{ $0 }, id: \.self) { key in
- Section(header: self.makeSearchCategoryHeader(category: key)) {
- ForEach(self.viewModel.searchResults[key] ?? []) { item in
- NavigationLink(destination: ItemDetailView(item: item)) {
- ItemRowView(item: item)
+ searchList
}
}
+ .navigationBarTitle(Text("Catalog"), displayMode: .inline)
+ .background(Color.grassBackground)
+
+ ItemsListView(viewModel: ItemsViewModel(category: .housewares))
}
}
+
+ private var categoriesList: some View {
+ List {
+ makeNatureCell()
+ makeWardrobeCell()
+ makeCategories()
+ }.modifier(DismissingKeyboardOnSwipe())
}
+
+ private var searchList: some View {
+ List {
+ ForEach(searchCategories, id: \.0, content: searchSection)
}.modifier(DismissingKeyboardOnSwipe())
}
- .navigationBarTitle(Text("Catalog"), displayMode: .inline)
- .background(Color.grassBackground)
- ItemsListView(viewModel: ItemsViewModel(category: .housewares))
+ private var searchCategories: [(Category, [Item])] {
+ viewModel.searchResults
+ .map { $0 }
+ .sorted(by: \.key.rawValue)
+ }
+
+ private func searchSection(category: Category, items: [Item]) -> some View {
+ Section(header: self.makeSearchCategoryHeader(category: category)) {
+ ForEach(items, content: self.searchItemRow)
+ }
+ }
+
+ private func searchItemRow(item: Item) -> some View {
+ NavigationLink(destination: ItemDetailView(item: item)) {
+ ItemRowView(item: item)
}
}
}


message: feat(Categories): add loading activity indicator while filtering Categories

diff:
02F2D4472455D81C009741DA /* simple-orange@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 02F2D3F72455D81C009741DA /* simple-orange@3x.png */; };
02F2D4482455D81C009741DA /* simple-mint@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 02F2D3F82455D81C009741DA /* simple-mint@3x.png */; };
4C382EE8244E418800F446BA /* DismissingKeyboardOnSwipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */; };
+ 4C7FE78324574FB50011E8AB /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FE78224574FB50011E8AB /* ActivityIndicator.swift */; };
68345579244C994C0072FC0C /* LikeButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68345578244C994C0072FC0C /* LikeButtonView.swift */; };
6834557B244CA8E10072FC0C /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6834557A244CA8E10072FC0C /* URL.swift */; };
68345589244E4DEF0072FC0C /* ACNHEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 68345588244E4DEF0072FC0C /* ACNHEvents */; };
02F2D3F72455D81C009741DA /* simple-orange@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "simple-orange@3x.png"; sourceTree = "<group>"; };
02F2D3F82455D81C009741DA /* simple-mint@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "simple-mint@3x.png"; sourceTree = "<group>"; };
4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissingKeyboardOnSwipe.swift; sourceTree = "<group>"; };
+ 4C7FE78224574FB50011E8AB /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
68345578244C994C0072FC0C /* LikeButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeButtonView.swift; sourceTree = "<group>"; };
6834557A244CA8E10072FC0C /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
6834558A244E4E0B0072FC0C /* DashboardEventTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardEventTextView.swift; sourceTree = "<group>"; };
68345578244C994C0072FC0C /* LikeButtonView.swift */,
69CA5ED9244CBA8600B275B6 /* CalendarView.swift */,
69CA5F00244F1AD300B275B6 /* SectionHeaderView.swift */,
+ 4C7FE78224574FB50011E8AB /* ActivityIndicator.swift */,
);
path = shared;
sourceTree = "<group>";
69CA5EF6244DBFCB00B275B6 /* Fruit.swift in Sources */,
693E0B1B244C2B9300D20E46 /* AppUserDefault.swift in Sources */,
69BEA989243E68D400F02C61 /* UserCollection.swift in Sources */,
+ 4C7FE78324574FB50011E8AB /* ActivityIndicator.swift in Sources */,
6942FC37244988F80062FE8F /* SafariView.swift in Sources */,
693E0B1D244C315B00D20E46 /* DashboardViewModel.swift in Sources */,
69AF8C7C243E4D29001DD9CB /* Color.swift in Sources */,

diff:
@@ -13,18 +13,24 @@ import Combine
class CategoriesSearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var searchResults: [Category: [Item]] = [:]
+ @Published var isLoadingData = false
private var searchCancellable: AnyCancellable?
init() {
searchCancellable = $searchText
- .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
+ .handleEvents(receiveOutput: { [weak self] _ in self?.isLoadingData = true })
+ .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.map(itemsInCategory(with:))
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
- .sink(receiveValue: { [weak self] in self?.searchResults = $0})
+ .sink(receiveValue: { [weak self] in
+ guard let self = self else { return }
+ self.searchResults = $0
+ self.isLoadingData = false
+ })
}
private func itemsInCategory(with string: String) -> [Category: [Item]] {

diff:
@@ -12,6 +12,7 @@ struct CategoriesView: View {
let categories: [Category]
@ObservedObject var viewModel = CategoriesSearchViewModel()
+ @State var isLoadingData = false
func makeCategories() -> some View {
ForEach(categories, id: \.self) { categorie in
@@ -75,6 +76,7 @@ struct CategoriesView: View {
}
.navigationBarTitle(Text("Catalog"), displayMode: .inline)
.background(Color.grassBackground)
+ .onReceive(viewModel.$isLoadingData) { self.isLoadingData = $0 }
ItemsListView(viewModel: ItemsViewModel(category: .housewares))
}
@@ -89,8 +91,13 @@ struct CategoriesView: View {
}
private var searchList: some View {
+ ZStack {
List {
ForEach(searchCategories, id: \.0, content: searchSection)
+ }
+ loadingView
+ .opacity(isLoadingData ? 80/100 : 0)
+ .animation(.default)
}.modifier(DismissingKeyboardOnSwipe())
}
@@ -111,6 +118,15 @@ struct CategoriesView: View {
ItemRowView(item: item)
}
}
+
+ private var loadingView: some View {
+ VStack {
+ HStack { Spacer() }
+ Spacer()
+ ActivityIndicator(isAnimating: $isLoadingData, style: .large)
+ Spacer()
+ }.background(Color.dialogue)
+ }
}
struct CategoriesView_Previews: PreviewProvider {


message: fix(Turnips): add missing .pbxproj modifications

diff:
02F2D4482455D81C009741DA /* simple-mint@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 02F2D3F82455D81C009741DA /* simple-mint@3x.png */; };
4C382EE8244E418800F446BA /* DismissingKeyboardOnSwipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */; };
4C7FE78324574FB50011E8AB /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FE78224574FB50011E8AB /* ActivityIndicator.swift */; };
+ 4C7FE789245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FE788245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift */; };
68345579244C994C0072FC0C /* LikeButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68345578244C994C0072FC0C /* LikeButtonView.swift */; };
68345589244E4DEF0072FC0C /* ACNHEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 68345588244E4DEF0072FC0C /* ACNHEvents */; };
6834558B244E4E0B0072FC0C /* DashboardEventTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6834558A244E4E0B0072FC0C /* DashboardEventTextView.swift */; };
02F2D3F82455D81C009741DA /* simple-mint@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "simple-mint@3x.png"; sourceTree = "<group>"; };
4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissingKeyboardOnSwipe.swift; sourceTree = "<group>"; };
4C7FE78224574FB50011E8AB /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
+ 4C7FE788245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptsToSoftwareKeyboard.swift; sourceTree = "<group>"; };
68345578244C994C0072FC0C /* LikeButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeButtonView.swift; sourceTree = "<group>"; };
6834558A244E4E0B0072FC0C /* DashboardEventTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardEventTextView.swift; sourceTree = "<group>"; };
687B5767244A030700C1E86F /* TurnipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnipsView.swift; sourceTree = "<group>"; };
isa = PBXGroup;
children = (
4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */,
+ 4C7FE788245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift */,
);
path = viewModifiers;
sourceTree = "<group>";
4C382EE8244E418800F446BA /* DismissingKeyboardOnSwipe.swift in Sources */,
69B13CA6245598E80089BD7B /* ItemDetailSeasonSectionView.swift in Sources */,
697E63F92449DADE008FB710 /* VillagerRowView.swift in Sources */,
+ 4C7FE789245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift in Sources */,
69B13CAD24577A090089BD7B /* TurnipIslandRow.swift in Sources */,
6834558B244E4E0B0072FC0C /* DashboardEventTextView.swift in Sources */,
693E0B1F244C526A00D20E46 /* ProgressView.swift in Sources */,


message: refactor(Turnips): remove unsused variable

diff:
@@ -16,9 +16,6 @@ struct TurnipsFormView: View {
@State private var fields = TurnipFields.decode()
@State private var enableNotifications = true
- private let labels = ["Monday AM", "Monday PM", "Tuesday AM", "Tuesday PM", "Wednesday AM", "Wednesday PM",
- "Thursday AM", "Thursday PM", "Friday AM", "Friday PM", "Saturday AM", "Saturday PM"]
-
private let weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
private var saveButton: some View {


message: feat(Turnips): add Turnips chart part 1

diff:
02F2D4462455D81C009741DA /* round-alt-orange@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 02F2D3F62455D81C009741DA /* round-alt-orange@3x.png */; };
02F2D4472455D81C009741DA /* simple-orange@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 02F2D3F72455D81C009741DA /* simple-orange@3x.png */; };
02F2D4482455D81C009741DA /* simple-mint@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 02F2D3F82455D81C009741DA /* simple-mint@3x.png */; };
+ 4C2D676B245DA41E005831C4 /* TurnipsChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2D676A245DA41E005831C4 /* TurnipsChartView.swift */; };
4C382EE8244E418800F446BA /* DismissingKeyboardOnSwipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */; };
4C7FE78324574FB50011E8AB /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FE78224574FB50011E8AB /* ActivityIndicator.swift */; };
4C7FE789245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FE788245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift */; };
02F2D3F62455D81C009741DA /* round-alt-orange@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "round-alt-orange@3x.png"; sourceTree = "<group>"; };
02F2D3F72455D81C009741DA /* simple-orange@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "simple-orange@3x.png"; sourceTree = "<group>"; };
02F2D3F82455D81C009741DA /* simple-mint@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "simple-mint@3x.png"; sourceTree = "<group>"; };
+ 4C2D676A245DA41E005831C4 /* TurnipsChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnipsChartView.swift; sourceTree = "<group>"; };
4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissingKeyboardOnSwipe.swift; sourceTree = "<group>"; };
4C7FE78224574FB50011E8AB /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
4C7FE788245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptsToSoftwareKeyboard.swift; sourceTree = "<group>"; };
69B13CAC24577A090089BD7B /* TurnipIslandRow.swift */,
687B5767244A030700C1E86F /* TurnipsView.swift */,
69B13CB024577BBF0089BD7B /* TurnipsFormView.swift */,
+ 4C2D676A245DA41E005831C4 /* TurnipsChartView.swift */,
);
path = turnips;
sourceTree = "<group>";
6942FC3A244989530062FE8F /* ListingRow.swift in Sources */,
69BEA97F243E602C00F02C61 /* TabbarView.swift in Sources */,
69B13CA42455973F0089BD7B /* ItemsCrosslineSectionView.swift in Sources */,
+ 4C2D676B245DA41E005831C4 /* TurnipsChartView.swift in Sources */,
693E0B22244C596D00D20E46 /* ActiveCrittersView.swift in Sources */,
69AF8C51243E283D001DD9CB /* AppDelegate.swift in Sources */,
693E728E245D71DD00CD26F4 /* UIState.swift in Sources */,

diff:
@@ -42,6 +42,13 @@ struct TurnipsView: View {
.foregroundColor(.blue)
}
}
+ if viewModel.predictions != nil {
+ Section(header: SectionHeaderView(text: "Chart")) {
+ NavigationLink(destination: TurnipsChartView(predictions: viewModel.predictions!)) {
+ Text("Chart").foregroundColor(.blue)
+ }
+ }
+ }
predictionsSection
exchangeSection
}

original_message: feat(Turnips): add Turnips chart part 1


message: feat(Turnips): add Turnips chart part 2

diff:
@@ -10,20 +10,23 @@ import SwiftUI
import Backend
struct TurnipsChartView: View {
+ static let verticalLinesCount: CGFloat = 9
var predictions: TurnipPredictions
var body: some View {
VStack {
Text("Turnips chart of the week")
Spacer()
- curves.padding()
+ curves
+ .padding()
+ .background(Color.red.opacity(0.1))
}
}
var curves: some View {
- HStack {
+ HStack(spacing: 0) {
TurnipsLegend(predictions: predictions)
- .frame(maxWidth: 40)
+ .frame(maxWidth: 40) // FIXME: should use something more dynamic
ZStack {
TurnipsGrid(predictions: predictions)
.stroke()
@@ -32,31 +35,47 @@ struct TurnipsChartView: View {
.stroke(lineWidth: 2)
TurnipsBuyPrice(predictions: predictions)
.stroke(lineWidth: 5)
- }.background(Color.blue.opacity(0.1))
}
+ .background(Color.blue.opacity(0.1))
}
}
+}
+
+struct TurnipsChartView_Previews: PreviewProvider {
+ static var previews: some View {
+ TurnipsChartView(predictions: TurnipPredictions(
+ minBuyPrice: 104,
+ averagePrices: averagePrices,
+ minMax: minMax)
+ )
+ }
+
+ static let averagePrices = [104, 93, 87, 88, 106, 110, 90, 74, 76, 91, 98, 96]
+
+ static let minMax = [[40, 148], [89, 148], [61, 148], [61, 86], [50, 148], [40, 210], [50, 626], [40, 626], [40, 626], [40, 626], [35, 210], [30, 209]]
+}
+
struct TurnipsLegend: View {
let predictions: TurnipPredictions
var body: some View {
- // TODO: this is ugly and should be refactored with some functions and computed var
- GeometryReader<AnyView> { geometry in
- let frame = geometry.frame(in: .local)
- let (minY, maxY, ratioY) = minMaxAndRatioY(for: self.predictions, rect: frame)
- return AnyView(
- ZStack {
- ForEach(Array(stride(from: minY, to: maxY, by: 50)), id: \.self) { value in
- Text("\(Int(value))")
- .offset(x: 0, y: ratioY * (value - minY))
- .offset(x: 0, y: -10) // FIXME: hardcoded value here! Should be computed
+ GeometryReader(content: texts)
+ .background(Color.yellow.opacity(0.1))
}
+
+ func texts(for geometry: GeometryProxy) -> some View {
+ let frame = geometry.frame(in: .local)
+ let (minY, maxY, ratioY) = minMaxAndRatioY(for: predictions, rect: frame)
+ let buyPrice = predictions.minBuyPrice ?? 0
+
+ return ZStack {
+ Text("\(Int(minY))")
+ .position(x: frame.midX, y: ratioY * (maxY - minY))
+ Text("\(Int(buyPrice))")
+ .position(x: frame.midX, y: ratioY * (maxY - CGFloat(buyPrice)))
Text("\(Int(maxY))")
- .offset(x: 0, y: ratioY * (maxY - minY))
- .offset(x: 0, y: -10) // FIXME: hardcoded value here! Should be computed
- }
- )
+ .position(x: frame.midX, y: 0)
}
}
}
@@ -77,7 +96,8 @@ struct TurnipsGrid: Shape {
path.addLine(to: CGPoint(x: offset * ratioX, y: ratioY * (maxY - minY)))
}
- for offset in stride(from: minY, to: maxY, by: 50) {
+ let steps = (maxY - minY)/TurnipsChartView.verticalLinesCount
+ for offset in stride(from: minY, to: maxY, by: steps) {
let offset = CGFloat(offset)
path.move(to: CGPoint(x: rect.minX, y: (offset - minY) * ratioY))
path.addLine(to: CGPoint(x: rect.maxX, y: (offset - minY) * ratioY))
@@ -89,20 +109,6 @@ struct TurnipsGrid: Shape {
}
}
-struct TurnipsChartView_Previews: PreviewProvider {
- static var previews: some View {
- TurnipsChartView(predictions: TurnipPredictions(
- minBuyPrice: 104,
- averagePrices: averagePrices,
- minMax: minMax)
- )
- }
-
- static let averagePrices = [104, 93, 87, 88, 106, 110, 90, 74, 76, 91, 98, 96]
-
- static let minMax = [[40, 148], [89, 148], [61, 148], [61, 86], [50, 148], [40, 210], [50, 626], [40, 626], [40, 626], [40, 626], [35, 210], [30, 209]]
-}
-
struct TurnipsAverageCurve: Shape {
static let averagesCount: CGFloat = 12
let predictions: TurnipPredictions
@@ -120,7 +126,7 @@ struct TurnipsAverageCurve: Shape {
let points = averagePrices.enumerated().map { offset, average -> CGPoint in
let x = ratioX * CGFloat(offset)
- let y = ratioY * (CGFloat(average) - minY)
+ let y = ratioY * (maxY - CGFloat(average))
return CGPoint(x: x, y: y)
}
path.addLines(points)
@@ -140,15 +146,16 @@ struct TurnipsBuyPrice: Shape {
return path
}
- let (min, max, ratioY) = minMaxAndRatioY(for: predictions, rect: rect)
+ let (minY, maxY, ratioY) = minMaxAndRatioY(for: predictions, rect: rect)
- let y = ratioY * (CGFloat(minBuyPrice) - min)
+ let y = ratioY * (maxY - CGFloat(minBuyPrice))
path.move(to: CGPoint(x: rect.minX, y: y))
path.addLine(to: CGPoint(x: rect.maxX, y: y))
return path
}
}
+// TODO: Put this function into a namespace
fileprivate func minMaxAndRatioY(
for predictions: TurnipPredictions,
rect: CGRect

original_message: feat(Turnips): add Turnips chart part 2


message: feat(Turnips): add Turnips chart part 3

diff:
@@ -33,10 +33,14 @@ struct TurnipsChartView: View {
.opacity(0.5)
TurnipsAverageCurve(predictions: predictions)
.stroke(lineWidth: 2)
+ .foregroundColor(PredictionCurve.average.color)
TurnipsBuyPrice(predictions: predictions)
- .stroke(lineWidth: 5)
- }
- .background(Color.blue.opacity(0.1))
+ .stroke(style: StrokeStyle(dash: [Self.verticalLinesCount]))
+ .foregroundColor(PredictionCurve.minBuyPrice.color)
+ TurnipsMinMaxCurves(predictions: predictions)
+ .foregroundColor(PredictionCurve.minMax.color)
+ .opacity(0.25)
+ }.background(Color.blue.opacity(0.1))
}
}
}
@@ -71,10 +75,13 @@ struct TurnipsLegend: View {
return ZStack {
Text("\(Int(minY))")
+ .foregroundColor(TurnipsChartView.PredictionCurve.minMax.color)
.position(x: frame.midX, y: ratioY * (maxY - minY))
Text("\(Int(buyPrice))")
+ .foregroundColor(TurnipsChartView.PredictionCurve.minBuyPrice.color)
.position(x: frame.midX, y: ratioY * (maxY - CGFloat(buyPrice)))
Text("\(Int(maxY))")
+ .foregroundColor(TurnipsChartView.PredictionCurve.minMax.color)
.position(x: frame.midX, y: 0)
}
}
@@ -135,6 +142,50 @@ struct TurnipsAverageCurve: Shape {
}
}
+struct TurnipsMinMaxCurves: Shape {
+ static let minMaxCount: CGFloat = 12
+ let predictions: TurnipPredictions
+
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+
+ guard let minMax = predictions.minMax else {
+ // Maybe we should just crash as it shouldn't happen
+ return path
+ }
+ let (minY, maxY, ratioY) = minMaxAndRatioY(for: predictions, rect: rect)
+
+ let ratioX = rect.maxX/(Self.minMaxCount - 1)
+
+ let minPoints: [CGPoint] = minMax
+ .compactMap { $0.first }
+ .enumerated()
+ .map({ offset, minValue in
+ let x = ratioX * CGFloat(offset)
+ let y = ratioY * (maxY - CGFloat(minValue))
+ return CGPoint(x: x, y: y)
+ })
+ path.addLines(minPoints)
+
+ let maxPoints: [CGPoint] = minMax
+ .compactMap { $0.second }
+ .enumerated()
+ .map({ offset, maxValue in
+ let x = ratioX * CGFloat(offset)
+ let y = ratioY * (maxY - CGFloat(maxValue))
+ return CGPoint(x: x, y: y)
+ })
+ .reversed()
+ dump(maxPoints)
+ path.addLine(to: maxPoints.first ?? .zero)
+ path.addLines(maxPoints)
+
+ path.addLine(to: minPoints.first ?? .zero)
+
+ return path
+ }
+}
+
struct TurnipsBuyPrice: Shape {
let predictions: TurnipPredictions
@@ -177,6 +228,23 @@ fileprivate func minMaxAndRatioY(
return (min, max, ratioY)
}
+extension TurnipsChartView {
+ enum PredictionCurve: String {
+ case minBuyPrice
+ case average
+ case minMax
+
+ var color: Color {
+ // TODO: replace by proper colors
+ switch self {
+ case .minBuyPrice: return .green
+ case .average: return .yellow
+ case .minMax: return .green
+ }
+ }
+ }
+}
+
extension Array {
var second: Element? {
self.count > 1 ? self[1] : nil

original_message: feat(Turnips): add Turnips chart part 3


message: feat(Turnips): add Turnips chart part 4

diff:
@@ -14,16 +14,6 @@ struct TurnipsChartView: View {
var predictions: TurnipPredictions
var body: some View {
- VStack {
- Text("Turnips chart of the week")
- Spacer()
- curves
- .padding()
- .background(Color.red.opacity(0.1))
- }
- }
-
- var curves: some View {
HStack(spacing: 0) {
TurnipsLegend(predictions: predictions)
.frame(maxWidth: 40) // FIXME: should use something more dynamic
@@ -45,21 +35,6 @@ struct TurnipsChartView: View {
}
}
-struct TurnipsChartView_Previews: PreviewProvider {
- static var previews: some View {
- TurnipsChartView(predictions: TurnipPredictions(
- minBuyPrice: 104,
- averagePrices: averagePrices,
- minMax: minMax)
- )
- }
-
- static let averagePrices = [104, 93, 87, 88, 106, 110, 90, 74, 76, 91, 98, 96]
-
- static let minMax = [[40, 148], [89, 148], [61, 148], [61, 86], [50, 148], [40, 210], [50, 626], [40, 626], [40, 626], [40, 626], [35, 210], [30, 209]]
-}
-
-
struct TurnipsLegend: View {
let predictions: TurnipPredictions
@@ -154,6 +129,7 @@ struct TurnipsMinMaxCurves: Shape {
return path
}
let (minY, maxY, ratioY) = minMaxAndRatioY(for: predictions, rect: rect)
+ print(minMax)
let ratioX = rect.maxX/(Self.minMaxCount - 1)
@@ -176,7 +152,6 @@ struct TurnipsMinMaxCurves: Shape {
return CGPoint(x: x, y: y)
})
.reversed()
- dump(maxPoints)
path.addLine(to: maxPoints.first ?? .zero)
path.addLines(maxPoints)
@@ -250,3 +225,17 @@ extension Array {
self.count > 1 ? self[1] : nil
}
}
+
+struct TurnipsChartView_Previews: PreviewProvider {
+ static var previews: some View {
+ TurnipsChartView(predictions: TurnipPredictions(
+ minBuyPrice: 83,
+ averagePrices: averagePrices,
+ minMax: minMax)
+ ).padding()
+ }
+
+ static let averagePrices = [89, 85, 88, 104, 110, 111, 111, 111, 106, 98, 82, 77]
+
+ static let minMax = [[38, 142], [33, 142], [29, 202], [24, 602], [19, 602], [14, 602], [9, 602], [29, 602], [24, 602], [19, 602], [14, 202], [9, 201]]
+}

original_message: feat(Turnips): add Turnips chart part 4


message: feat(Turnips): remove y-axis grid for chart

diff:
@@ -24,15 +24,6 @@ struct TurnipsChartGrid: Shape {
path.addLine(to: CGPoint(x: offset * ratioX, y: ratioY * (maxY - minY)))
}
- let steps = (maxY - minY)/TurnipsChartView.verticalLinesCount
- for offset in stride(from: minY, to: maxY, by: steps) {
- let offset = CGFloat(offset)
- path.move(to: CGPoint(x: rect.minX, y: (offset - minY) * ratioY))
- path.addLine(to: CGPoint(x: rect.maxX, y: (offset - minY) * ratioY))
- }
- path.move(to: CGPoint(x: rect.minX, y: (maxY - minY) * ratioY))
- path.addLine(to: CGPoint(x: rect.maxX, y: (maxY - minY) * ratioY))
-
return path
}
}


message: refactor(Turnips): remove useless code from Turnips Chart

diff:
import SwiftUI
import Backend
-// TODO: refactor by renaming this extension to TurnipsChart namespace
enum TurnipsChart {
enum PredictionCurve: String {
case minBuyPrice
@@ -27,10 +26,6 @@ enum TurnipsChart {
}
}
-extension TurnipsChart {
-
-}
-
extension Array {
var second: Element? {
self.count > 1 ? self[1] : nil


message: feat(Turnips Chart): add vertical legend with strided value to 50

diff:
4C2D6776245F4F55005831C4 /* TurnipsChartMinMaxCurves.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2D6775245F4F55005831C4 /* TurnipsChartMinMaxCurves.swift */; };
4C2D6778245F4FE8005831C4 /* TurnipsChartMinBuyPriceCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2D6777245F4FE8005831C4 /* TurnipsChartMinBuyPriceCurve.swift */; };
4C382EE8244E418800F446BA /* DismissingKeyboardOnSwipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */; };
+ 4C7F2F782461F10300930928 /* TurnipsChartVerticalLegend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7F2F772461F10300930928 /* TurnipsChartVerticalLegend.swift */; };
4C7FE78324574FB50011E8AB /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FE78224574FB50011E8AB /* ActivityIndicator.swift */; };
4C7FE789245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FE788245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift */; };
68345579244C994C0072FC0C /* LikeButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68345578244C994C0072FC0C /* LikeButtonView.swift */; };
4C2D6775245F4F55005831C4 /* TurnipsChartMinMaxCurves.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnipsChartMinMaxCurves.swift; sourceTree = "<group>"; };
4C2D6777245F4FE8005831C4 /* TurnipsChartMinBuyPriceCurve.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnipsChartMinBuyPriceCurve.swift; sourceTree = "<group>"; };
4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissingKeyboardOnSwipe.swift; sourceTree = "<group>"; };
+ 4C7F2F772461F10300930928 /* TurnipsChartVerticalLegend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnipsChartVerticalLegend.swift; sourceTree = "<group>"; };
4C7FE78224574FB50011E8AB /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
4C7FE788245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptsToSoftwareKeyboard.swift; sourceTree = "<group>"; };
68345578244C994C0072FC0C /* LikeButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeButtonView.swift; sourceTree = "<group>"; };
4C2D676C245F063E005831C4 /* charts */ = {
isa = PBXGroup;
children = (
- 4C2D676A245DA41E005831C4 /* TurnipsChartView.swift */,
- 69EC82ED2461E79D00CE9064 /* TurnipsChartTopLegendView.swift */,
- 4C2D676D245F0666005831C4 /* TurnipsChartBottomLegendView.swift */,
4C2D676F245F0698005831C4 /* TurnipsChart.swift */,
- 4C2D6771245F0C9D005831C4 /* TurnipsChartGrid.swift */,
4C2D6773245F4EA2005831C4 /* TurnipsChartAverageCurve.swift */,
- 4C2D6775245F4F55005831C4 /* TurnipsChartMinMaxCurves.swift */,
+ 4C2D676D245F0666005831C4 /* TurnipsChartBottomLegendView.swift */,
+ 4C2D6771245F0C9D005831C4 /* TurnipsChartGrid.swift */,
4C2D6777245F4FE8005831C4 /* TurnipsChartMinBuyPriceCurve.swift */,
+ 4C2D6775245F4F55005831C4 /* TurnipsChartMinMaxCurves.swift */,
+ 69EC82ED2461E79D00CE9064 /* TurnipsChartTopLegendView.swift */,
+ 4C7F2F772461F10300930928 /* TurnipsChartVerticalLegend.swift */,
+ 4C2D676A245DA41E005831C4 /* TurnipsChartView.swift */,
);
path = charts;
sourceTree = "<group>";
693E0B22244C596D00D20E46 /* ActiveCrittersView.swift in Sources */,
693E72A2245EA84500CD26F4 /* View.swift in Sources */,
69AF8C51243E283D001DD9CB /* AppDelegate.swift in Sources */,
+ 4C7F2F782461F10300930928 /* TurnipsChartVerticalLegend.swift in Sources */,
693E728E245D71DD00CD26F4 /* UIState.swift in Sources */,
68BA5834244B9D4100B53811 /* DashboardView.swift in Sources */,
692C0B8524517837004847A1 /* DashboardCollectionProgressSection.swift in Sources */,

diff:
@@ -18,14 +18,27 @@ struct TurnipsChartView: View {
var body: some View {
VStack {
TurnipsChartTopLegendView()
- ScrollView(.horizontal) {
- chart.frame(width: 600, height: 400)
+ ScrollView(.horizontal, showsIndicators: false) {
+ chart.frame(width: 600, height: 500)
}
}
}
private var chart: some View {
- VStack(spacing: 0) {
+ VStack(spacing: 10) {
+ HStack {
+ TurnipsChartVerticalLegend(predictions: predictions)
+ .frame(width: 40)
+ curves
+ }
+ TurnipsChartBottomLegendView(predictions: predictions).frame(height: 50)
+ }
+ .padding()
+ .navigationBarItems(trailing: closeButton)
+ .navigationBarTitle("Turnips chart for the week", displayMode: .inline)
+ }
+
+ private var curves: some View {
ZStack {
TurnipsChartGrid(predictions: predictions)
.stroke()
@@ -40,11 +53,6 @@ struct TurnipsChartView: View {
.stroke(lineWidth: 3)
.foregroundColor(PredictionCurve.average.color)
}
- TurnipsChartBottomLegendView(predictions: predictions).frame(height: 50)
- }
- .padding()
- .navigationBarItems(trailing: closeButton)
- .navigationBarTitle("Turnips chart for the week", displayMode: .inline)
}
private var closeButton: some View {


message: fix(Turnips): remove old code related to Chart Navigation behaviour

diff:
@@ -32,10 +32,7 @@ struct TurnipsChartView: View {
curves
}
TurnipsChartBottomLegendView(predictions: predictions).frame(height: 50)
- }
- .padding()
- .navigationBarItems(trailing: closeButton)
- .navigationBarTitle("Turnips chart for the week", displayMode: .inline)
+ }.padding()
}
private var curves: some View {
@@ -54,12 +51,6 @@ struct TurnipsChartView: View {
.foregroundColor(PredictionCurve.average.color)
}
}
-
- private var closeButton: some View {
- Button(action: { self.presentation.wrappedValue.dismiss() }) {
- Text("Close")
- }
- }
}
struct TurnipsChartView_Previews: PreviewProvider {


message: refactor(Turnips): move Path extension for Hermite curve into its own file

diff:
4C7F2F782461F10300930928 /* TurnipsChartVerticalLegend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7F2F772461F10300930928 /* TurnipsChartVerticalLegend.swift */; };
4C7FE78324574FB50011E8AB /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FE78224574FB50011E8AB /* ActivityIndicator.swift */; };
4C7FE789245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FE788245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift */; };
+ 4CF8049B24635E54005C6C63 /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF8049A24635E54005C6C63 /* Path.swift */; };
68345579244C994C0072FC0C /* LikeButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68345578244C994C0072FC0C /* LikeButtonView.swift */; };
68345589244E4DEF0072FC0C /* ACNHEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 68345588244E4DEF0072FC0C /* ACNHEvents */; };
6834558B244E4E0B0072FC0C /* DashboardEventTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6834558A244E4E0B0072FC0C /* DashboardEventTextView.swift */; };
4C7F2F772461F10300930928 /* TurnipsChartVerticalLegend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnipsChartVerticalLegend.swift; sourceTree = "<group>"; };
4C7FE78224574FB50011E8AB /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
4C7FE788245B57A10011E8AB /* AdaptsToSoftwareKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptsToSoftwareKeyboard.swift; sourceTree = "<group>"; };
+ 4CF8049A24635E54005C6C63 /* Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Path.swift; sourceTree = "<group>"; };
68345578244C994C0072FC0C /* LikeButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeButtonView.swift; sourceTree = "<group>"; };
6834558A244E4E0B0072FC0C /* DashboardEventTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardEventTextView.swift; sourceTree = "<group>"; };
687B5767244A030700C1E86F /* TurnipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnipsView.swift; sourceTree = "<group>"; };
children = (
693E72A1245EA84400CD26F4 /* View.swift */,
693E729F245EA5A000CD26F4 /* Text.swift */,
+ 4CF8049A24635E54005C6C63 /* Path.swift */,
);
path = extensions;
sourceTree = "<group>";
693E72A2245EA84500CD26F4 /* View.swift in Sources */,
69AF8C51243E283D001DD9CB /* AppDelegate.swift in Sources */,
4C7F2F782461F10300930928 /* TurnipsChartVerticalLegend.swift in Sources */,
+ 4CF8049B24635E54005C6C63 /* Path.swift in Sources */,
693E728E245D71DD00CD26F4 /* UIState.swift in Sources */,
68BA5834244B9D4100B53811 /* DashboardView.swift in Sources */,
692C0B8524517837004847A1 /* DashboardCollectionProgressSection.swift in Sources */,

diff:
@@ -47,51 +47,3 @@ struct TurnipsChartMinMaxCurves: Shape {
return path
}
}
-
-private extension CGPoint {
- static func +(lhs: Self, rhs: Self) -> Self {
- Self(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
- }
-
- static func -(lhs: Self, rhs: Self) -> Self {
- Self(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
- }
-
- static func /(lhs: Self, rhs: CGFloat) -> Self {
- Self(x: lhs.x/rhs, y: lhs.y/rhs)
- }
-
- static func *(lhs: Self, rhs: CGFloat) -> Self {
- Self(x: lhs.x * rhs, y: lhs.y * rhs)
- }
-}
-
-// Inspired from https://github.com/FlexMonkey/SmoothScribble/blob/master/SmoothScribble/extensions/UIBezierPathExtension.swift
-extension Path {
- mutating func addHermiteCurvedLines(points : [CGPoint], alpha : CGFloat = 1/3) {
- guard !points.isEmpty else { return }
- move(to: points.first ?? .zero)
-
- for (i, currentPoint) in points.enumerated() {
- if currentPoint == points.last { continue }
-
- let previousPoint = points[max(0, i - 1)]
- let nextPoint = points[min(i + 1, points.count - 1)]
-
- let previousDerivative = (nextPoint - previousPoint)/2
- let control1 = currentPoint + previousDerivative * alpha
-
- let furtherPoint = points[min(i + 2, points.count - 1)]
-
- let nextDerivative: CGPoint
- if i < points.count - 1 {
- nextDerivative = (furtherPoint - currentPoint)/2
- } else {
- nextDerivative = (nextPoint - currentPoint)/2
- }
- let control2 = nextPoint - nextDerivative * alpha
-
- self.addCurve(to: nextPoint, control1: control1, control2: control2)
- }
- }
-}


message: feat(Turnips): Chart - add fancy animation for minMax curves

diff:
@@ -23,6 +23,11 @@ struct TurnipsView: View {
case .chart: return "Chart"
}
}
+
+ var isChart: Bool {
+ get { self == .chart }
+ set { }
+ }
}
private enum Sheet: String, Identifiable {
@@ -174,7 +179,10 @@ extension TurnipsView {
}
} else if turnipsDisplay == .chart {
if viewModel.predictions != nil {
- TurnipsChartView(predictions: viewModel.predictions!).padding(.top, 8)
+ TurnipsChartView(
+ predictions: viewModel.predictions!,
+ animateCurves: $turnipsDisplay.isChart
+ ).padding(.top, 8)
} else {
Text("Add your in game turnip prices to see the predictions chart")
.foregroundColor(.bell)

diff:
@@ -11,6 +11,12 @@ import Backend
struct TurnipsChartMinMaxCurves: Shape {
let predictions: TurnipPredictions
+ var animationStep: CGFloat = 1
+
+ var animatableData: CGFloat {
+ get { animationStep }
+ set { animationStep = newValue }
+ }
func path(in rect: CGRect) -> Path {
var path = Path()
@@ -26,7 +32,7 @@ struct TurnipsChartMinMaxCurves: Shape {
.enumerated()
.map({ offset, minValue in
let x = ratioX * CGFloat(offset)
- let y = ratioY * (maxY - CGFloat(minValue))
+ let y = ratioY * (maxY - CGFloat(minValue) * animationStep)
return CGPoint(x: x, y: y)
})
path.addHermiteCurvedLines(points: minPoints)
@@ -36,7 +42,7 @@ struct TurnipsChartMinMaxCurves: Shape {
.enumerated()
.map({ offset, maxValue in
let x = ratioX * CGFloat(offset)
- let y = ratioY * (maxY - CGFloat(maxValue))
+ let y = ratioY * (maxY - CGFloat(maxValue) * animationStep)
return CGPoint(x: x, y: y)
})
.reversed()
@@ -47,3 +53,26 @@ struct TurnipsChartMinMaxCurves: Shape {
return path
}
}
+
+struct TurnipsChartMinMaxCurves_Previews: PreviewProvider {
+ static var previews: some View {
+ Preview().padding()
+ }
+
+ private struct Preview: View {
+ @State private var animationStep: CGFloat = 1
+
+ var body: some View {
+ VStack {
+ TurnipsChartMinMaxCurves(
+ predictions: TurnipsChartView_Previews.predictions,
+ animationStep: animationStep
+ )
+ .foregroundColor(TurnipsChart.PredictionCurve.minMax.color)
+ .opacity(0.25)
+ .blendMode(.darken)
+ Slider(value: $animationStep, in: 0.1...1)
+ }
+ }
+ }
+}

diff:
@@ -13,6 +13,7 @@ struct TurnipsChartView: View {
typealias PredictionCurve = TurnipsChart.PredictionCurve
static let verticalLinesCount: CGFloat = 9
var predictions: TurnipPredictions
+ @Binding var animateCurves: Bool
@Environment(\.presentationMode) var presentation
var body: some View {
@@ -47,7 +48,7 @@ struct TurnipsChartView: View {
.foregroundColor(PredictionCurve.minBuyPrice.color)
.saturation(3)
.blendMode(.screen)
- TurnipsChartMinMaxCurves(predictions: predictions)
+ TurnipsChartMinMaxCurves(predictions: predictions, animationStep: animateCurves ? 1 : 0.1)
.foregroundColor(PredictionCurve.minMax.color)
.opacity(0.25)
.blendMode(.darken)
@@ -56,19 +57,24 @@ struct TurnipsChartView: View {
.foregroundColor(PredictionCurve.average.color)
.saturation(5)
.blendMode(.screen)
- }
+ }.animation(.spring())
}
}
struct TurnipsChartView_Previews: PreviewProvider {
static var previews: some View {
- TurnipsChartView(predictions: TurnipPredictions(
+ TurnipsChartView(
+ predictions: predictions,
+ animateCurves: .constant(true)
+ ).padding()
+ }
+
+ static let predictions = TurnipPredictions(
minBuyPrice: 83,
averagePrices: averagePrices,
minMax: minMax,
averageProfits: averageProfits
- )).padding()
- }
+ )
static let averagePrices = [89, 85, 88, 104, 110, 111, 111, 111, 106, 98, 82, 77]


message: feat(Turnips): Chart - animate average curve (trimming)

diff:
@@ -11,6 +11,12 @@ import Backend
struct TurnipsChartAverageCurve: Shape {
let predictions: TurnipPredictions
+ var animationStep: CGFloat = 1
+
+ var animatableData: CGFloat {
+ get { animationStep }
+ set { animationStep = newValue }
+ }
func path(in rect: CGRect) -> Path {
var path = Path()
@@ -29,6 +35,30 @@ struct TurnipsChartAverageCurve: Shape {
}
path.addLines(points)
- return path
+ return path.trimmedPath(from: 0, to: animationStep)
+ }
+}
+
+
+struct TurnipsChartAverageCurve_Previews: PreviewProvider {
+ static var previews: some View {
+ Preview().padding()
+ }
+
+ private struct Preview: View {
+ @State private var animationStep: CGFloat = 1
+
+ var body: some View {
+ VStack {
+ TurnipsChartAverageCurve(
+ predictions: TurnipsChartView_Previews.predictions,
+ animationStep: animationStep
+ )
+ .stroke(lineWidth: 3)
+ .foregroundColor(TurnipsChart.PredictionCurve.average.color)
+ .saturation(5)
+ Slider(value: $animationStep, in: 0...1)
+ }
+ }
}
}

diff:
@@ -52,7 +52,7 @@ struct TurnipsChartView: View {
.foregroundColor(PredictionCurve.minMax.color)
.opacity(0.25)
.blendMode(.darken)
- TurnipsChartAverageCurve(predictions: predictions)
+ TurnipsChartAverageCurve(predictions: predictions, animationStep: animateCurves ? 1 : 0)
.stroke(lineWidth: 3)
.foregroundColor(PredictionCurve.average.color)
.saturation(5)


message: refactor(Turnips): start using Alignment Guide for Chart Legend

diff:
@@ -11,6 +11,8 @@ import Backend
struct TurnipsChartGrid: Shape {
let predictions: TurnipPredictions
+ // TODO: put only .vertical after tests
+ let displays: Set<Display> = [.vertical, .horizontal]
func path(in rect: CGRect) -> Path {
var path = Path()
@@ -18,12 +20,42 @@ struct TurnipsChartGrid: Shape {
let (minY, maxY, ratioY, ratioX) = predictions.minMax?.roundedMinMaxAndRatios(rect: rect) ?? (0, 0, 0, 0)
let count = predictions.minMax?.count ?? 0
+ if displays.contains(.vertical) {
for offset in 0..<count {
let offset = CGFloat(offset)
path.move(to: CGPoint(x: offset * ratioX, y: rect.minY))
path.addLine(to: CGPoint(x: offset * ratioX, y: ratioY * (maxY - minY)))
}
+ }
+
+ if displays.contains(.horizontal) {
+ let min = Int(minY)
+ let max = Int(maxY) + TurnipsChart.steps
+ let lines = Array(stride(from: min, to: max, by: TurnipsChart.steps))
+ for line in lines {
+ let line = CGFloat(line)
+ path.move(to: CGPoint(x: rect.minX, y: ratioY * line))
+ path.addLine(to: CGPoint(x: rect.maxX, y: ratioY * line))
+ }
+ }
return path
}
}
+
+extension TurnipsChartGrid {
+ enum Display { case vertical, horizontal }
+}
+
+extension VerticalAlignment {
+ private enum VerticalLegendAlignment: AlignmentID {
+ static func defaultValue(in context: ViewDimensions) -> CGFloat {
+ context[VerticalAlignment.center]
+ }
+ }
+ static let verticalLegendAlignment = VerticalAlignment(VerticalLegendAlignment.self)
+}
+
+extension Alignment {
+ static let verticalLegendAlignment = Alignment(horizontal: .trailing, vertical: .verticalLegendAlignment)
+}

diff:
@@ -20,14 +20,18 @@ struct TurnipsChartVerticalLegend: View {
let frame = geometry.frame(in: .local)
let (minY, maxY, ratioY, _) = predictions.minMax?.roundedMinMaxAndRatios(rect: frame) ?? (0, 0, 0, 0)
- return ZStack {
- ForEach(Array(stride(from: Int(minY), to: Int(maxY), by: TurnipsChart.steps)), id: \.self) { value in
+ let min = Int(minY)
+ let max = Int(maxY) + TurnipsChart.steps
+ let values = Array(stride(from: min, to: max, by: TurnipsChart.steps))
+
+ return ZStack(alignment: .verticalLegendAlignment) {
+ ForEach(values, id: \.self) { value in
Text("\(value)")
.font(.footnote)
.fontWeight(.semibold)
.foregroundColor(.text)
- .position(x: frame.midX, y: (maxY - CGFloat(value)) * ratioY)
- }
- }
+ .alignmentGuide(.verticalLegendAlignment) { d in CGFloat(value) * ratioY }
+ }.background(Color.red)
+ }.background(Color.blue)
}
}

diff:
@@ -66,7 +66,7 @@ struct TurnipsChartView_Previews: PreviewProvider {
TurnipsChartView(
predictions: predictions,
animateCurves: .constant(true)
- ).padding()
+ )
}
static let predictions = TurnipPredictions(


message:
refactor(Turnips): remove useless custom Alignment Guide
* After better understanding of how Aligment Guide works, it's useless
to create a custom one for ZStack, I can directly use .topTrailing +
.top as custom explicit Alignment

diff:
@@ -46,16 +46,3 @@ struct TurnipsChartGrid: Shape {
extension TurnipsChartGrid {
enum Display { case vertical, horizontal }
}
-
-extension VerticalAlignment {
- private enum VerticalLegendAlignment: AlignmentID {
- static func defaultValue(in context: ViewDimensions) -> CGFloat {
- context[VerticalAlignment.center]
- }
- }
- static let verticalLegendAlignment = VerticalAlignment(VerticalLegendAlignment.self)
-}
-
-extension Alignment {
- static let verticalLegendAlignment = Alignment(horizontal: .trailing, vertical: .verticalLegendAlignment)
-}

diff:
@@ -11,27 +11,29 @@ import Backend
struct TurnipsChartVerticalLegend: View {
let predictions: TurnipPredictions
-
var body: some View {
- GeometryReader(content: texts)
+ GeometryReader(content: computeTexts)
}
- func texts(geometry: GeometryProxy) -> some View {
+ func computeTexts(geometry: GeometryProxy) -> some View {
let frame = geometry.frame(in: .local)
let (minY, maxY, ratioY, _) = predictions.minMax?.roundedMinMaxAndRatios(rect: frame) ?? (0, 0, 0, 0)
let min = Int(minY)
let max = Int(maxY) + TurnipsChart.steps
let values = Array(stride(from: min, to: max, by: TurnipsChart.steps))
+ return texts(values: values, ratioY: ratioY)
+ }
- return ZStack(alignment: .verticalLegendAlignment) {
+ func texts(values: [Int], ratioY: CGFloat) -> some View {
+ ZStack(alignment: .topTrailing) {
ForEach(values, id: \.self) { value in
Text("\(value)")
.font(.footnote)
.fontWeight(.semibold)
.foregroundColor(.text)
- .alignmentGuide(.verticalLegendAlignment) { d in CGFloat(value) * ratioY }
- }.background(Color.red)
- }.background(Color.blue)
+ .alignmentGuide(.top) { d in CGFloat(value) * ratioY }
+ }
+ }
}
}


message: refactor(Turnips): use Alignment Guide for Chart Bottom Legend

diff:
@@ -41,7 +41,7 @@ extension Array where Element == [Int] {
private func minMaxAndRatios(
rect: CGRect
) -> (min: CGFloat, max: CGFloat, ratioY: CGFloat, ratioX: CGFloat) {
- let ratioX = rect.maxX/(CGFloat(self.count) - 1)
+ let ratioX = rect.maxX/(CGFloat(self.count - 1))
let min: CGFloat = self
.compactMap { $0.first }

diff:
@@ -14,40 +14,52 @@ struct TurnipsChartBottomLegendView: View {
private let weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
var body: some View {
- GeometryReader(content: texts)
+ GeometryReader(content: computeTexts)
}
- func texts(for geometry: GeometryProxy) -> some View {
- let frame = geometry.frame(in: .local)
- let (_, _, _, ratioX) = predictions.minMax?.roundedMinMaxAndRatios(rect: frame) ?? (0, 0, 0, 0)
+ func computeTexts(for geometry: GeometryProxy) -> some View {
+ let rect = geometry.frame(in: .local)
+ let (_, _, _, ratioX) = predictions.minMax?.roundedMinMaxAndRatios(rect: rect) ?? (0, 0, 0, 0)
let count = predictions.minMax?.count ?? 0
+ return texts(count: count, ratioX: ratioX)
+ }
- return VStack {
- ZStack {
+ func texts(count: Int, ratioX: CGFloat) -> some View {
+ VStack {
+ ZStack(alignment: .leading) {
ForEach(0..<count) { offset in
- self.yAxisText(offset: offset, ratioX: ratioX, frame: frame)
+ self.meridiem(offset: offset, ratioX: ratioX)
}
}
- ZStack {
+ ZStack(alignment: .leading) {
ForEach(Array(weekdays.enumerated()), id: \.0) { offset, weekday in
- // TODO: investigate AlignmentGuide instead of guessing the exact position
- Text(weekday)
- .font(.callout)
- .fontWeight(.bold)
- .foregroundColor(.text)
- .position(x: CGFloat(offset + 1) * ratioX/2 + CGFloat(offset) * ratioX * 1.5, y: frame.midY)
+ self.weekdays(offset: offset, weekday: weekday, ratioX: ratioX)
}
}
}
}
- // TODO: investigate AlignmentGuide instead of guessing the exact position
- func yAxisText(offset: Int, ratioX: CGFloat, frame: CGRect) -> some View {
- let isAM = offset == 0 || offset.isMultiple(of: 2)
- return Text(isAM ? "AM" : "PM")
+ func meridiem(offset: Int, ratioX: CGFloat) -> some View {
+ Text(offset.isAM ? "AM" : "PM")
.font(.footnote)
.fontWeight(.semibold)
.foregroundColor(.text)
- .position(x: CGFloat(offset) * ratioX, y: frame.midY)
+ .alignmentGuide(.leading, computeValue: { d in
+ -CGFloat(offset) * ratioX
+ })
}
+
+ func weekdays(offset: Int, weekday: String, ratioX: CGFloat) -> some View {
+ Text(weekday)
+ .font(.callout)
+ .fontWeight(.bold)
+ .foregroundColor(.text)
+ .alignmentGuide(.leading, computeValue: { d in
+ -CGFloat(offset * 2 + 1) * ratioX + d.width/2
+ })
+ }
+}
+
+private extension Int {
+ var isAM: Bool { self == 0 || isMultiple(of: 2) }
}

diff:
@@ -27,13 +27,13 @@ struct TurnipsChartView: View {
private var chart: some View {
VStack(spacing: 10) {
- HStack {
+ HStack(spacing: 8) {
TurnipsChartVerticalLegend(predictions: predictions)
.frame(width: 30)
curves
}
TurnipsChartBottomLegendView(predictions: predictions)
- .padding(.leading, 30)
+ .padding(.leading, 38)
.frame(height: 50)
}.padding()
}


message:
refactor(Turnips): use less magic values for Chart
* Basically instead of hardcode the ChartWidth for bottom
Legend it uses the chart width computed like we do with
PreferenceKey (but no need of PreferenceKey here as it's in the same `struct`

diff:
@@ -11,8 +11,7 @@ import Backend
struct TurnipsChartGrid: Shape {
let predictions: TurnipPredictions
- // TODO: put only .vertical after tests
- let displays: Set<Display> = [.vertical, .horizontal]
+ let displays: Set<Display> = [.vertical]
func path(in rect: CGRect) -> Path {
var path = Path()

diff:
@@ -15,26 +15,30 @@ struct TurnipsChartView: View {
var predictions: TurnipPredictions
@Binding var animateCurves: Bool
@Environment(\.presentationMode) var presentation
+ @State private var chartWidth: CGFloat = 0
var body: some View {
VStack {
TurnipsChartTopLegendView()
+ HStack {
ScrollView(.horizontal, showsIndicators: false) {
chart.frame(width: 600, height: 500)
}
}
}
+ }
private var chart: some View {
- VStack(spacing: 10) {
- HStack(spacing: 8) {
+ VStack(alignment: .custom, spacing: 10) {
+ HStack {
TurnipsChartVerticalLegend(predictions: predictions)
.frame(width: 30)
curves
+ .alignmentGuide(.custom, computeValue: { $0[HorizontalAlignment.leading] })
+ .background(curvesGeometry)
}
TurnipsChartBottomLegendView(predictions: predictions)
- .padding(.leading, 38)
- .frame(height: 50)
+ .frame(width: chartWidth, height: 50)
}.padding()
}
@@ -59,6 +63,24 @@ struct TurnipsChartView: View {
.blendMode(.screen)
}.animation(.spring())
}
+
+ private var curvesGeometry: some View {
+ GeometryReader { geometry in
+ Rectangle()
+ .fill(Color.clear)
+ .onAppear(perform: { self.chartWidth = geometry.size.width })
+ }
+ }
+}
+
+private extension HorizontalAlignment {
+ private struct CustomAlignment: AlignmentID {
+ static func defaultValue(in context: ViewDimensions) -> CGFloat {
+ return context[HorizontalAlignment.leading]
+ }
+ }
+
+ static let custom = HorizontalAlignment(CustomAlignment.self)
}
struct TurnipsChartView_Previews: PreviewProvider {


message:
fix(Turnips): put back hardocded value for Bottom Legend Height
* Add a comment to explain why I rolled back this. It's far from ideal
but as I don't know so much about Anchor Preference, I would take time
to dive deeper into it and come with a concrete solution later on.

diff:
@@ -24,9 +24,14 @@ struct TurnipsChartBottomLegendView: View {
var body: some View {
GeometryReader(content: computeTexts)
- .onHeightPreferenceChange(TextsHeightPreferenceKey.self, storeValueIn: $textsHeight)
- .frame(height: self.textsHeight)
- .fixedSize(horizontal: false, vertical: true)
+ // TOOD: actually use onHeightPreferenceChange instead of hardcoded value.
+ // Put back hardcoded value to fix a bump that occurs when the view appears.
+ // In the debugger we see a `Bound preference TextsHeightPreferenceKey tried to update multiple times per frame.` problem
+ // maybe it's related, we should dive deeper into Anchor Preference.
+ .frame(height: 50)
+// .onHeightPreferenceChange(TextsHeightPreferenceKey.self, storeValueIn: $textsHeight)
+// .frame(height: self.textsHeight)
+// .fixedSize(horizontal: false, vertical: true)
}
func computeTexts(for geometry: GeometryProxy) -> some View {

original_message:
fix(Turnips): put back hardocded value for Bottom Legend Height (#117)

* Add a comment to explain why I rolled back this. It's far from ideal
but as I don't know so much about Anchor Preference, I would take time
to dive deeper into it and come with a concrete solution later on.


message:
feat(Turnips): use average color for entered displayed values in Chart
* Previously, it was the minMax color value because it was on the bottom
of the ZStack. As average capsule color is more readable IMO, we choose
this one

diff:
@@ -21,17 +21,17 @@ struct TurnipsChartValuesView: View {
.foregroundColor(.graphMinMax)
.modifier(ValueModifier(backgroundColor: Color.acBackground))
.position(data[position].min.position)
+ Text("\(data[position].max.value)")
+ .bold()
+ .foregroundColor(.graphMinMax)
+ .modifier(ValueModifier(backgroundColor: Color.acBackground))
+ .position(data[position].max.position)
Text("\(data[position].average.value)")
.bold()
.foregroundColor(.graphAverage)
.saturation(5)
.modifier(ValueModifier(backgroundColor: Color.acHeaderBackground))
.position(data[position].average.position)
- Text("\(data[position].max.value)")
- .bold()
- .foregroundColor(.graphMinMax)
- .modifier(ValueModifier(backgroundColor: Color.acBackground))
- .position(data[position].max.position)
}
}
}

original_message:
feat(Turnips): use average color for entered displayed values in Chart (#181)

* Previously, it was the minMax color value because it was on the bottom
of the ZStack. As average capsule color is more readable IMO, we choose
this one


diff:
@@ -92,13 +92,18 @@ public class UserCollection: ObservableObject {
return added
}
- public func
- updateProgress(taskName: DailyTasks.taskName) {
+ public func updateProgress(taskName: DailyTasks.taskName) {
dailyTasks.tasks[taskName]?.curProgress += 1
dailyTasks.lastUpdate = Date()
save()
}
+ public func resetProgress(taskName: DailyTasks.taskName) {
+ dailyTasks.tasks[taskName]?.curProgress = 0
+ dailyTasks.lastUpdate = Date()
+ save()
+ }
+
public func resetTasks() {
dailyTasks.lastUpdate = Date()
for(taskName, _) in dailyTasks.tasks {

diff:
@@ -50,6 +50,10 @@ struct TodayTasksSection: View {
}
self.collection.updateProgress(taskName: taskName)
}
+ .onLongPressGesture {
+ guard task.hasProgress else { return }
+ self.collection.resetProgress(taskName: taskName)
+ }
}
var body: some View {

original_message: feat(Today Dashboard): add Long Press gesture to reset progress on Today tasks (#192)


message:
fix(Dashboard Today Widgets): reorder works without crash
* Sometimes a crash could happen (especially when you try to reorder
Tasks to the first elements in the list)

diff:
import Foundation
-public struct TodaySection: Codable, Hashable, Equatable {
- public static let nameEvents = "events"
- public static let nameSpecialCharacters = "specialCharacters"
- public static let nameCurrentlyAvailable = "currentlyAvailable"
- public static let nameCollectionProgress = "collectionProgress"
- public static let nameBirthdays = "birthdays"
- public static let nameTurnips = "turnips"
- public static let nameSubscribe = "subscribe"
- public static let nameMysteryIsland = "mysteryIsland"
- public static let nameMusic = "music"
- public static let nameTasks = "tasks"
- public static let nameNookazon = "nookazon"
-
- public static let defaultSectionList: [TodaySection] = [
- TodaySection(name: Self.nameEvents, enabled: true),
- TodaySection(name: Self.nameSpecialCharacters, enabled: true),
- TodaySection(name: Self.nameCurrentlyAvailable, enabled: true),
- TodaySection(name: Self.nameCollectionProgress, enabled: true),
- TodaySection(name: Self.nameBirthdays, enabled: true),
- TodaySection(name: Self.nameTurnips, enabled: true),
- TodaySection(name: Self.nameTasks, enabled: true),
- TodaySection(name: Self.nameSubscribe, enabled: true),
- TodaySection(name: Self.nameMusic, enabled: true),
- //TodaySection(name: Self.nameNookazon, enabled: true)
- TodaySection(name: Self.nameMysteryIsland, enabled: true),
- ]
-
- public static func == (lhs: TodaySection, rhs: TodaySection) -> Bool {
- return lhs.name == rhs.name
- }
+public struct TodaySection: Codable, Hashable, Identifiable {
+ public let name: Name
+ public var enabled: Bool
+ public var id: Name { name }
- public init(name: String, enabled: Bool) {
+ public init(name: Name, enabled: Bool) {
self.name = name
self.enabled = enabled
}
+}
- public let name: String
- public var enabled: Bool
+extension TodaySection {
+ public enum Name: String, Codable {
+ case events
+ case specialCharacters
+ case currentlyAvailable
+ case collectionProgress
+ case birthdays
+ case turnips
+ case subscribe
+ case mysteryIsland
+ case music
+ case tasks
+ case nookazon
+ }
+
+ public static let defaultSectionList: [TodaySection] = [
+ TodaySection(name: .events, enabled: true),
+ TodaySection(name: .specialCharacters, enabled: true),
+ TodaySection(name: .currentlyAvailable, enabled: true),
+ TodaySection(name: .collectionProgress, enabled: true),
+ TodaySection(name: .birthdays, enabled: true),
+ TodaySection(name: .turnips, enabled: true),
+ TodaySection(name: .tasks, enabled: true),
+ TodaySection(name: .subscribe, enabled: true),
+ TodaySection(name: .music, enabled: true),
+ //TodaySection(name: .nameNookazon, enabled: true)
+ TodaySection(name: .mysteryIsland, enabled: true),
+ ]
}

diff:
@@ -15,7 +15,7 @@ class DashboardViewModel: ObservableObject {
@Published var island: Island?
@Published var sectionOrder: [TodaySection]
- public var selection = Set<String>()
+ public var selection = Set<TodaySection.Name>()
private var listingCancellable: AnyCancellable?
private var islandCancellable: AnyCancellable?
@@ -25,9 +25,8 @@ class DashboardViewModel: ObservableObject {
}
public func saveSectionList() {
- for index in 0..<sectionOrder.count {
- let name = sectionOrder[index].name
- if !selection.contains(name) && name != TodaySection.nameSubscribe {
+ for (index, section) in sectionOrder.enumerated() {
+ if !selection.contains(section.name) && section.name != .subscribe {
sectionOrder[index].enabled = false
} else {
sectionOrder[index].enabled = true
@@ -43,11 +42,7 @@ class DashboardViewModel: ObservableObject {
sectionOrder.append(TodaySection(name: section.name, enabled: true))
}
}
- sectionOrder.forEach { (section) in
- if section.enabled {
- selection.insert(section.name)
- }
- }
+ selection = Set(sectionOrder.filter(\.enabled).map(\.name))
}
func fetchListings() {

diff:
import SwiftUI
import Backend
+import Combine
struct TodaySectionEditView: View {
@Environment(\.presentationMode) private var presentationMode
@@ -16,34 +17,6 @@ struct TodaySectionEditView: View {
@State private var editMode = EditMode.active
- private let textForSectionName = [
- TodaySection.nameEvents: "Events",
- TodaySection.nameSpecialCharacters: "Possible visitors",
- TodaySection.nameCurrentlyAvailable: "Currently Available",
- TodaySection.nameCollectionProgress: "Collection Progress",
- TodaySection.nameBirthdays: "Today's Birthdays",
- TodaySection.nameTurnips: "Turnips",
- TodaySection.nameSubscribe: "Subscribe",
- TodaySection.nameMysteryIsland: "Mystery Islands",
- TodaySection.nameMusic: "Music player",
- TodaySection.nameTasks: "Today's Tasks",
- TodaySection.nameNookazon: "New on Nookazon"
- ]
-
- private let iconForSectionName = [
- TodaySection.nameEvents: "flag.fill",
- TodaySection.nameSpecialCharacters: "clock",
- TodaySection.nameCurrentlyAvailable: "calendar",
- TodaySection.nameCollectionProgress: "chart.pie.fill",
- TodaySection.nameBirthdays: "gift.fill",
- TodaySection.nameTurnips: "dollarsign.circle.fill",
- TodaySection.nameSubscribe: "suit.heart.fill",
- TodaySection.nameMysteryIsland: "sun.haze.fill",
- TodaySection.nameMusic: "music.note",
- TodaySection.nameTasks: "checkmark.seal.fill",
- TodaySection.nameNookazon: "cart.fill"
- ]
-
var body: some View {
List(selection: self.$viewModel.selection) {
Section(header: SectionHeaderView(text: "Drag & Drop to Rearrange",
@@ -51,13 +24,13 @@ struct TodaySectionEditView: View {
footer: self.footer) {
ForEach(viewModel.sectionOrder, id: \.name) { section in
HStack {
- Image(systemName: self.iconForSectionName[section.name] ?? "")
+ Image(systemName: section.iconName)
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(width: 22)
- .foregroundColor(Color("ACSecondaryText"))
+ .foregroundColor(.acSecondaryText)
- Text(LocalizedStringKey(self.textForSectionName[section.name] ?? ""))
+ Text(LocalizedStringKey(section.sectionName))
.font(.system(.body, design: .rounded))
Spacer()
@@ -97,6 +70,40 @@ struct TodaySectionEditView: View {
}
}
+extension TodaySection {
+ var sectionName: String {
+ switch name {
+ case .events: return "Events"
+ case .specialCharacters: return "Possible visitors"
+ case .currentlyAvailable: return "Currently Available"
+ case .collectionProgress: return "Collection Progress"
+ case .birthdays: return "Today's Birthdays"
+ case .turnips: return "Turnips"
+ case .subscribe: return "Subscribe"
+ case .mysteryIsland: return "Mystery Islands"
+ case .music: return "Music player"
+ case .tasks: return "Today's Tasks"
+ case .nookazon: return "New on Nookazon"
+ }
+ }
+
+ var iconName: String {
+ switch name {
+ case .events: return "flag.fill"
+ case .specialCharacters: return "clock"
+ case .currentlyAvailable: return "calendar"
+ case .collectionProgress: return "chart.pie.fill"
+ case .birthdays: return "gift.fill"
+ case .turnips: return "dollarsign.circle.fill"
+ case .subscribe: return "suit.heart.fill"
+ case .mysteryIsland: return "sun.haze.fill"
+ case .music: return "music.note"
+ case .tasks: return "checkmark.seal.fill"
+ case .nookazon: return "cart.fill"
+ }
+ }
+}
+
struct TodaySectionEditView_Previews: PreviewProvider {
static var previews: some View {
TodaySectionEditView(viewModel: DashboardViewModel())

diff:
@@ -28,34 +28,31 @@ struct TodaySectionView: View {
}
switch(section.name) {
- case TodaySection.nameEvents:
+ case .events:
return AnyView(TodayEventsSection())
- case TodaySection.nameSpecialCharacters:
+ case .specialCharacters:
return AnyView(TodaySpecialCharactersSection())
- case TodaySection.nameCurrentlyAvailable:
+ case .currentlyAvailable:
return AnyView(TodayCurrentlyAvailableSection(viewModel: viewModel))
- case TodaySection.nameCollectionProgress:
+ case .collectionProgress:
return AnyView(TodayCollectionProgressSection(viewModel: viewModel, sheet: $selectedSheet))
- case TodaySection.nameBirthdays:
+ case .birthdays:
return AnyView(TodayBirthdaysSection())
- case TodaySection.nameTurnips:
+ case .turnips:
return AnyView(TodayTurnipSection()
.onTapGesture {
self.uiState.selectedTab = .turnips
})
- case TodaySection.nameSubscribe:
+ case .subscribe:
return AnyView(TodaySubscribeSection(sheet: $selectedSheet))
- case TodaySection.nameMysteryIsland:
+ case .mysteryIsland:
return AnyView(TodayMysteryIslandsSection())
- case TodaySection.nameMusic:
+ case .music:
return AnyView(TodayMusicPlayerSection())
- case TodaySection.nameTasks:
+ case .tasks:
return AnyView(TodayTasksSection())
- case TodaySection.nameNookazon:
+ case .nookazon:
return AnyView(TodayNookazonSection(sheet: $selectedSheet, viewModel: viewModel))
- default:
- return AnyView(EmptyView())
}
-
}
}

original_message:
fix(Dashboard Today Widgets): reorder works without crash (#194)

* Sometimes a crash could happen (especially when you try to reorder
Tasks to the first elements in the list)


diff:
@@ -49,10 +49,12 @@ struct TodayTasksSection: View {
return
}
self.collection.updateProgress(taskName: taskName)
+ FeedbackGenerator.shared.triggerSelection()
}
.onLongPressGesture {
guard task.hasProgress else { return }
self.collection.resetProgress(taskName: taskName)
+ FeedbackGenerator.shared.triggerSelection()
}
}
@@ -87,7 +89,6 @@ struct TodayTasksSection: View {
.padding(.vertical, 8)
}
}
-
}
struct TodayTasksSection_Previews: PreviewProvider {

original_message: feat(Today Dashboard - Tasks): add haptic feedback on touch or long press (#203)


message:
refactor(Share): improve code of sharing feature
* When trying to work on but cannot fix the initial issue. At
least these improvements can be merged

diff:
3D9AB093248257DF00B47041 /* CustomTaskFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9AB092248257DF00B47041 /* CustomTaskFormViewModel.swift */; };
3DECE21B2483BC83001F24BA /* EditMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DECE21A2483BC82001F24BA /* EditMode.swift */; };
4C16FB41247AC9B0009F24E3 /* GridStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C16FB40247AC9B0009F24E3 /* GridStack.swift */; };
+ 4C30BFC3249627FD003E96A2 /* Sequence+Unique.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30BFC2249627FC003E96A2 /* Sequence+Unique.swift */; };
4C6E95FD24842F690074433B /* Collection+SafeDirectAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6E95FC24842F690074433B /* Collection+SafeDirectAccess.swift */; };
4C7F555D248B91C80089F26C /* TodayVillagerVisitsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7F555C248B91C80089F26C /* TodayVillagerVisitsSection.swift */; };
690A72C924752BF4001E7294 /* villagersLikes in Resources */ = {isa = PBXBuildFile; fileRef = 690A72C824752BF4001E7294 /* villagersLikes */; };
4C2D6773245F4EA2005831C4 /* TurnipsChartAverageCurve.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnipsChartAverageCurve.swift; sourceTree = "<group>"; };
4C2D6775245F4F55005831C4 /* TurnipsChartMinMaxCurves.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnipsChartMinMaxCurves.swift; sourceTree = "<group>"; };
4C2D6777245F4FE8005831C4 /* TurnipsChartMinBuyPriceCurve.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnipsChartMinBuyPriceCurve.swift; sourceTree = "<group>"; };
+ 4C30BFC2249627FC003E96A2 /* Sequence+Unique.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Unique.swift"; sourceTree = "<group>"; };
4C382EE7244E418800F446BA /* DismissingKeyboardOnSwipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissingKeyboardOnSwipe.swift; sourceTree = "<group>"; };
4C6E95FC24842F690074433B /* Collection+SafeDirectAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+SafeDirectAccess.swift"; sourceTree = "<group>"; };
4C7F2F772461F10300930928 /* TurnipsChartVerticalLegend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnipsChartVerticalLegend.swift; sourceTree = "<group>"; };
693E2BC7246697FC00B85CB8 /* Image.swift */,
3DECE21A2483BC82001F24BA /* EditMode.swift */,
4C6E95FC24842F690074433B /* Collection+SafeDirectAccess.swift */,
+ 4C30BFC2249627FC003E96A2 /* Sequence+Unique.swift */,
);
path = extensions;
sourceTree = "<group>";
69157BC92471A5A1005B9002 /* TurnipsChartGrid.swift in Sources */,
4C7F555D248B91C80089F26C /* TodayVillagerVisitsSection.swift in Sources */,
69157BCA2471A5A1005B9002 /* CollectionRowView.swift in Sources */,
+ 4C30BFC3249627FD003E96A2 /* Sequence+Unique.swift in Sources */,
AE0B5B5C247AD7590075CF16 /* DesignFormViewModel.swift in Sources */,
AE51C2E324801D5D00F074EC /* ChoreListRowView.swift in Sources */,
69E67EF0249376DF00ED7AC8 /* DodoCodeRow.swift in Sources */,

diff:
@@ -11,18 +11,19 @@ import SwiftUI
#if !os(tvOS)
public struct ActivityControllerView: UIViewControllerRepresentable {
-
- public let activityItems: [Any]
- public let applicationActivities: [UIActivity]?
-
- public init(activityItems: [Any], applicationActivities: [UIActivity]?) {
- self.activityItems = activityItems
- self.applicationActivities = applicationActivities
- }
+ @Environment(\.presentationMode) var presentation
+ public var activityItems: [Any]
+ public var applicationActivities: [UIActivity]?
public func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityControllerView>) -> UIActivityViewController {
- return UIActivityViewController(activityItems: activityItems,
- applicationActivities: applicationActivities)
+ let controller = UIActivityViewController(
+ activityItems: activityItems,
+ applicationActivities: applicationActivities
+ )
+ controller.completionWithItemsHandler = { _, _, _, _ in
+ self.presentation.wrappedValue.dismiss()
+ }
+ return controller
}
public func updateUIViewController(_ uiViewController: UIActivityViewController,

diff:
@@ -37,7 +37,10 @@ public class VillagerDetailViewModel: ObservableObject {
self?.preferredItems = items
})
- self.likes = Array(Set(Items.shared.villagersLike.values.first(where: { $0.id == filename })!.likes))
+ self.likes = Items.shared.villagersLike.values
+ .first(where: { $0.id == filename })?
+ .likes
+ .unique()
} else {
self.villagerItems = nil

diff:
@@ -32,7 +32,6 @@ class ItemDetailSource: NSObject, UIActivityItemSource {
metadata.title = name
metadata.imageProvider = NSItemProvider(object: image)
- metadata.imageProvider = NSItemProvider(object: image)
return metadata
}

diff:
@@ -11,7 +11,8 @@ import Backend
struct Sheet: View {
enum SheetType: Identifiable {
- case safari(URL), share(content: [Any])
+ case safari(URL)
+ case share(content: [Any])
case about
case userListForm(editingList: UserList?)
case customTasks(collection: UserCollection)

original_message:
refactor(Share): improve code of sharing feature (#260)

* When trying to work on #251, but cannot fix the initial issue. At
least these improvements can be merged


diff:
@@ -61,6 +61,7 @@ struct ActiveCritterSections: View {
}
struct ActiveCrittersView: View {
+ @Environment(\.currentDate) private var currentDate
@StateObject private var viewModel = ActiveCrittersViewModel(filterOutInCollection: true)
@State private var selectedTab: ActiveCrittersViewModel.CritterType
@@ -105,6 +106,9 @@ struct ActiveCrittersView: View {
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.navigationBarTitle("Active Critters", displayMode: .inline)
+ .onAppear {
+ viewModel.updateCritters(for: currentDate)
+ }
}
}

original_message:
fix(Active Critters): load critters when opening Active Critters View

* Previously, the loading of critters was not triggered, so it displayed
an activity indicators without endlessly
* Fix #330


message:
feat(Like Button): add half star if item is not completed
* Add an half star to show that the item has been partially completed,
meaning at least one of its variants is liked, but not all of them.

diff:
@@ -18,6 +18,7 @@ class LikeButtonViewModel: ObservableObject {
let villager: Villager?
@Published var isInCollection = false
+ @Published var variantsCompletionStatus: VariantsCompletionStatus = .unstarted
private let collection: UserCollection
private var cancellable: AnyCancellable?
@@ -42,6 +43,11 @@ class LikeButtonViewModel: ObservableObject {
self.isInCollection = variants.contains(for: item, variant: variant)
return
}
+ if item.hasSomeVariations {
+ self.variantsCompletionStatus = variants.completionStatus(for: item)
+ self.isInCollection = self.variantsCompletionStatus != .unstarted
+ return
+ }
self.isInCollection = items.contains(item) || critters.contains(item)
return
} else if let villager = villager {

diff:
@@ -25,6 +25,13 @@ struct LikeButtonView: View {
if viewModel.item?.isCritter == true {
return viewModel.isInCollection ? "checkmark.seal.fill" : "checkmark.seal"
} else {
+ if viewModel.item?.hasSomeVariations == true && viewModel.variant == nil {
+ switch viewModel.variantsCompletionStatus {
+ case .unstarted: return "star"
+ case .partial: return "star.leadinghalf.fill"
+ case .complete: return "star.fill"
+ }
+ }
return viewModel.isInCollection ? "star.fill" : "star"
}
} else {

diff:
@@ -499,10 +499,37 @@ public class UserCollection: ObservableObject {
}
public extension Dictionary where Key == String, Value == [Variant] {
- public func contains(for item: Item, variant: Variant) -> Bool {
+ func contains(for item: Item, variant: Variant) -> Bool {
guard let filename = item.filename else {
return false
}
return self[filename]?.contains(variant) == true
}
+
+ func completionStatus(for item: Item) -> VariantsCompletionStatus {
+ guard let filename = item.filename,
+ let itemVariations = item.variations,
+ let currentVariations = self[filename]
+ else { return .unstarted }
+
+ if currentVariations.count <= 0 {
+ return .unstarted
+ } else if currentVariations.count < itemVariations.count {
+ return .partial
+ } else {
+ return .complete
+ }
+ }
+}
+
+public enum VariantsCompletionStatus {
+ case unstarted
+ case partial
+ case complete
+}
+
+public extension Item {
+ var hasSomeVariations: Bool {
+ variations?.count ?? 0 > 1
+ }
}


message:
feat(Like Button): display bottom sheet variations
* Display bottom sheet variations if the item has variants instead of
adding it to the collection without their variants.

diff:
@@ -76,4 +76,8 @@ class LikeButtonViewModel: ObservableObject {
}
return added
}
+
+ var hasSomeVariations: Bool {
+ variant == nil && item?.hasSomeVariations == true
+ }
}

diff:
@@ -29,6 +29,7 @@ struct ItemsView: View {
@StateObject var viewModel: ItemsViewModel
@State private var contentMode: ContentMode = .listLarge
+ @State private var likedItemWithVariants: Item?
let customTitle: String?
init(category: Backend.Category, items: [Item]? = nil, keyword: String? = nil) {
@@ -110,6 +111,7 @@ struct ItemsView: View {
@ViewBuilder
private var contentView: some View {
+ ZStack {
if contentMode == .grid {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120), spacing: 16)], spacing: 16) {
@@ -125,8 +127,11 @@ struct ItemsView: View {
Section(header: SearchField(searchText: $viewModel.searchText)) {
ForEach(viewModel.items) { item in
NavigationLink(destination: LazyView(ItemDetailView(item: item))) {
- ItemRowView(displayMode: contentMode == .listLarge ? .large : .compact,
- item: item)
+ ItemRowView(
+ displayMode: contentMode == .listLarge ? .large : .compact,
+ item: item,
+ likedItemWithVariants: $likedItemWithVariants
+ )
}
.listRowBackground(Color.acSecondaryBackground)
}
@@ -135,6 +140,36 @@ struct ItemsView: View {
.listStyle(GroupedListStyle())
.id(viewModel.sort)
}
+ likedItemWithVariants.map { item in
+ ZStack {
+ Color.black
+ .opacity(1/6)
+ .onTapGesture { likedItemWithVariants = nil }
+ VStack {
+ Spacer()
+ itemVariants(for: item)
+ }
+ }
+ }
+ }
+ }
+
+ private func itemVariants(for item: Item) -> some View {
+ VStack {
+ SectionHeaderView(text: "Variants", icon: "paintbrush.fill").padding()
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack {
+ ForEach(item.variations ?? []) { variant in
+ ZStack(alignment: .topLeading) {
+ ItemImage(path: variant.content.image, size: 75)
+ LikeButtonView(item: item, variant: variant)
+ }
+ }
+ }.padding()
+ }
+ .listRowInsets(EdgeInsets())
+ }
+ .background(Color.acSecondaryBackground)
}
}

diff:
@@ -17,9 +17,19 @@ struct ItemRowView: View {
let displayMode: DisplayMode
let item: Item
-
+ @Binding var likedItemWithVariants: Item?
@State private var displayedVariant: Variant?
+ init(
+ displayMode: DisplayMode,
+ item: Item,
+ likedItemWithVariants: Binding<Item?> = .constant(nil)
+ ) {
+ self.displayMode = displayMode
+ self.item = item
+ self._likedItemWithVariants = likedItemWithVariants
+ }
+
private var imageSize: CGFloat {
switch displayMode {
case .compact:
@@ -87,7 +97,11 @@ struct ItemRowView: View {
var body: some View {
HStack(spacing: 8) {
if displayMode != .largeNoButton {
- LikeButtonView(item: item, variant: displayedVariant)
+ LikeButtonView(
+ item: item,
+ variant: displayedVariant,
+ likedItemWithVariants: $likedItemWithVariants
+ )
}
if item.finalImage == nil && displayedVariant == nil {
Image(item.appCategory.iconName())

diff:
@@ -11,13 +11,20 @@ import Backend
struct LikeButtonView: View {
@StateObject private var viewModel: LikeButtonViewModel
+ @Binding private var likedItemWithVariants: Item?
- init(item: Item, variant: Variant?) {
+ init(
+ item: Item,
+ variant: Variant?,
+ likedItemWithVariants: Binding<Item?> = .constant(nil)
+ ) {
_viewModel = StateObject(wrappedValue: LikeButtonViewModel(item: item, variant: variant))
+ _likedItemWithVariants = likedItemWithVariants
}
init(villager: Villager) {
_viewModel = StateObject(wrappedValue: LikeButtonViewModel(villager: villager))
+ _likedItemWithVariants = .constant(nil)
}
var imageName: String {
@@ -25,7 +32,7 @@ struct LikeButtonView: View {
if viewModel.item?.isCritter == true {
return viewModel.isInCollection ? "checkmark.seal.fill" : "checkmark.seal"
} else {
- if viewModel.item?.hasSomeVariations == true && viewModel.variant == nil {
+ if viewModel.hasSomeVariations {
switch viewModel.variantsCompletionStatus {
case .unstarted: return "star"
case .partial: return "star.leadinghalf.fill"
@@ -51,8 +58,12 @@ struct LikeButtonView: View {
var body: some View {
Button(action: {
+ if viewModel.hasSomeVariations {
+ likedItemWithVariants = viewModel.item
+ } else {
let added = self.viewModel.toggleCollection()
FeedbackGenerator.shared.triggerNotification(type: added ? .success : .warning)
+ }
}) {
Image(systemName: imageName).foregroundColor(color)
}


message:
fix(Like Button): mark the item as liked if one of its variant is liked
* Previously the item wasn't marked as liked if one of its variant is
liked

diff:
@@ -136,18 +136,29 @@ public class UserCollection: ObservableObject {
guard let filename = item.filename else {
return false
}
+ let isAdded: Bool
if variants[filename]?.contains(variant) == true {
variants[filename]?.removeAll(where: { $0 == variant })
save()
- return false
+ isAdded = false
} else {
if variants[filename] == nil {
variants[filename] = []
}
variants[filename]?.append(variant)
save()
- return true
+ isAdded = true
}
+
+ if !items.contains(item) && variants.completionStatus(for: item) != .unstarted {
+ // We add the item to the liked items if at least one of its variant is added
+ toggleItem(item: item)
+ } else if items.contains(item) && variants.completionStatus(for: item) == .unstarted {
+ // We remove the item from the liked items if there is none of its variants liked
+ toggleItem(item: item)
+ }
+
+ return isAdded
}
public func toggleCritters(critter: Item) -> Bool {