Improved how game detail view look in non edit mode

This commit is contained in:
2021-06-04 22:33:33 +02:00
parent 45ea335f3b
commit aa64cd0600
39 changed files with 816 additions and 286 deletions

View File

@@ -19,6 +19,9 @@
B94CB50422D1352F0029BFAD /* Game+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = B94CB4F822D1352F0029BFAD /* Game+CoreDataProperties.swift */; };
B94CB53722D3B3CC0029BFAD /* GameDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B94CB53622D3B3CC0029BFAD /* GameDetailView.swift */; };
B952648526602CB600BB4324 /* MainView_Zockerhoehle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B952648426602CB600BB4324 /* MainView_Zockerhoehle.swift */; };
B95264872661041D00BB4324 /* ConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95264862661041D00BB4324 /* ConsoleView.swift */; };
B95264A826610E0800BB4324 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B95264A726610E0800BB4324 /* Preview Assets.xcassets */; };
B969C5F0266693EE00401B89 /* GameEditMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B969C5EF266693EE00401B89 /* GameEditMode.swift */; };
B9839983233A086A002F9946 /* Overview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9839982233A086A002F9946 /* Overview.swift */; };
B98A734D22BAD27D00FB3410 /* Zockerhoehle.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B98A731722BA9E4600FB3410 /* Zockerhoehle.xcdatamodeld */; };
B98A736022C1738800FB3410 /* CDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98A735F22C1738800FB3410 /* CDManager.swift */; };
@@ -40,7 +43,7 @@
B9EC0987238555BF004BC9AB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9EC0986238555BF004BC9AB /* SettingsView.swift */; };
B9ED3DD7265534C000FD2D46 /* test_date_utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9ED3DD6265534C000FD2D46 /* test_date_utils.swift */; };
B9ED3DD9265D1E5600FD2D46 /* CDPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9ED3DD8265D1E5600FD2D46 /* CDPreview.swift */; };
B9ED3DDB265D47EC00FD2D46 /* GameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9ED3DDA265D47EB00FD2D46 /* GameView.swift */; };
B9ED3DDB265D47EC00FD2D46 /* GameCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9ED3DDA265D47EB00FD2D46 /* GameCover.swift */; };
B9F44ABA22F312E600FC6B29 /* ConsoleLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9F44AB922F312E600FC6B29 /* ConsoleLibraryView.swift */; };
/* End PBXBuildFile section */
@@ -84,6 +87,9 @@
B94CB4FA22D1352F0029BFAD /* Console+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Console+CoreDataProperties.swift"; sourceTree = "<group>"; };
B94CB53622D3B3CC0029BFAD /* GameDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameDetailView.swift; sourceTree = "<group>"; };
B952648426602CB600BB4324 /* MainView_Zockerhoehle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView_Zockerhoehle.swift; sourceTree = "<group>"; };
B95264862661041D00BB4324 /* ConsoleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleView.swift; sourceTree = "<group>"; };
B95264A726610E0800BB4324 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
B969C5EF266693EE00401B89 /* GameEditMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameEditMode.swift; sourceTree = "<group>"; };
B9839982233A086A002F9946 /* Overview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Overview.swift; sourceTree = "<group>"; };
B98A731822BA9E4600FB3410 /* Zockerhoehle.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Zockerhoehle.xcdatamodel; sourceTree = "<group>"; };
B98A735F22C1738800FB3410 /* CDManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CDManager.swift; sourceTree = "<group>"; };
@@ -106,7 +112,7 @@
B9EC0986238555BF004BC9AB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
B9ED3DD6265534C000FD2D46 /* test_date_utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = test_date_utils.swift; sourceTree = "<group>"; };
B9ED3DD8265D1E5600FD2D46 /* CDPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CDPreview.swift; sourceTree = "<group>"; };
B9ED3DDA265D47EB00FD2D46 /* GameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameView.swift; sourceTree = "<group>"; };
B9ED3DDA265D47EB00FD2D46 /* GameCover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameCover.swift; sourceTree = "<group>"; };
B9F44AB922F312E600FC6B29 /* ConsoleLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLibraryView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -146,13 +152,15 @@
B9A0550022F8C22D0054D9A0 /* GameSeriesAllView.swift */,
B93D60CD22D88F5700DD390F /* AccessoryDetailView.swift */,
B94CB53622D3B3CC0029BFAD /* GameDetailView.swift */,
B9ED3DDA265D47EB00FD2D46 /* GameView.swift */,
B969C5EF266693EE00401B89 /* GameEditMode.swift */,
B9ED3DDA265D47EB00FD2D46 /* GameCover.swift */,
B94112E3233B597D00159AE4 /* ConsoleEditView.swift */,
B9F44AB922F312E600FC6B29 /* ConsoleLibraryView.swift */,
B9E2A07C233B6E4F00EAEB14 /* ConsoleAllView.swift */,
B94112DF233A4EF800159AE4 /* GamePickupsView.swift */,
B9EC0986238555BF004BC9AB /* SettingsView.swift */,
B9839982233A086A002F9946 /* Overview.swift */,
B95264862661041D00BB4324 /* ConsoleView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -202,10 +210,19 @@
B93C1BA321496BFE0014FD6E /* Assets.xcassets */,
B93C1BA821496BFE0014FD6E /* Info.plist */,
B98A731722BA9E4600FB3410 /* Zockerhoehle.xcdatamodeld */,
B95264A626610E0800BB4324 /* Preview Content */,
);
path = Zockerhoehle;
sourceTree = "<group>";
};
B95264A626610E0800BB4324 /* Preview Content */ = {
isa = PBXGroup;
children = (
B95264A726610E0800BB4324 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
B98A734622BACA9C00FB3410 /* CDModel */ = {
isa = PBXGroup;
children = (
@@ -323,6 +340,7 @@
buildActionMask = 2147483647;
files = (
B9BCF4CA2168ACB600ECBAAC /* LaunchScreen.storyboard in Resources */,
B95264A826610E0800BB4324 /* Preview Assets.xcassets in Resources */,
B93C1BA421496BFE0014FD6E /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -378,6 +396,7 @@
B952648526602CB600BB4324 /* MainView_Zockerhoehle.swift in Sources */,
B98CBBDC264E98DE00B1B7AC /* GameSeries+CoreDataClass.swift in Sources */,
B9A0550122F8C22D0054D9A0 /* GameSeriesAllView.swift in Sources */,
B969C5F0266693EE00401B89 /* GameEditMode.swift in Sources */,
B94112E4233B597D00159AE4 /* ConsoleEditView.swift in Sources */,
B94112DE233A37DD00159AE4 /* DateConversion.swift in Sources */,
B93D60CE22D88F5700DD390F /* AccessoryDetailView.swift in Sources */,
@@ -387,9 +406,10 @@
B94CB50422D1352F0029BFAD /* Game+CoreDataProperties.swift in Sources */,
B98A736022C1738800FB3410 /* CDManager.swift in Sources */,
B9E2A07E233B6E4F00EAEB14 /* ConsoleAllView.swift in Sources */,
B95264872661041D00BB4324 /* ConsoleView.swift in Sources */,
B9BCCEB92653BDEA005F46D6 /* ICloudManager.swift in Sources */,
B9EC0987238555BF004BC9AB /* SettingsView.swift in Sources */,
B9ED3DDB265D47EC00FD2D46 /* GameView.swift in Sources */,
B9ED3DDB265D47EC00FD2D46 /* GameCover.swift in Sources */,
B9ED3DD9265D1E5600FD2D46 /* CDPreview.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -539,6 +559,7 @@
CODE_SIGN_ENTITLEMENTS = Zockerhoehle/Zockerhoehle.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "Zockerhoehle/Preview\\ Content";
DEVELOPMENT_TEAM = 85J8CBD673;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Zockerhoehle/Info.plist;
@@ -550,6 +571,8 @@
PRODUCT_BUNDLE_IDENTIFIER = haus.mueller.zockerhoehle;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@@ -562,6 +585,7 @@
CODE_SIGN_ENTITLEMENTS = Zockerhoehle/Zockerhoehle.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "Zockerhoehle/Preview\\ Content";
DEVELOPMENT_TEAM = M9N7K3KZX9;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Zockerhoehle/Info.plist;
@@ -573,6 +597,8 @@
PRODUCT_BUNDLE_IDENTIFIER = haus.mueller.zockerhoehle;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};

View File

@@ -22,7 +22,7 @@ public class Console: NSManagedObject, Identifiable {
Game.compareByCreationDate(gameA: $0, gameB: $1)
}) else { return false }
return newestGameConsoleA.createdAt > newestGameConsoleB.createdAt
return newestGameConsoleA.pickupOrReleaseDate > newestGameConsoleB.pickupOrReleaseDate
}
// Defining default values during creation

View File

@@ -15,14 +15,14 @@ import SwiftUI
public class Game: NSManagedObject, Identifiable {
public static func compareByCreationDate(gameA : Game, gameB : Game) -> Bool {
return gameA.createdAt < gameB.createdAt
return gameA.pickupOrReleaseDate < gameB.pickupOrReleaseDate
}
@objc
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
self.createdAt = Date()
self.pickupOrReleaseDate = Date()
self.uuid = UUID()
print("Set UUID to \(self.uuid)")
}
@@ -35,7 +35,8 @@ extension Game : Encodable {
case notes
case isDigital
case lentTo
case createdAt
case pickupOrReleaseDate
case isPickupDate
case publisher
case isFinished
case finishedDate
@@ -55,7 +56,8 @@ extension Game : Encodable {
try container.encode(notes ?? "", forKey: .notes)
try container.encode(isDigital, forKey: .isDigital)
try container.encode(lentTo ?? "", forKey: .lentTo)
try container.encode(createdAt.formattedInTimeZone(), forKey: .createdAt)
try container.encode(pickupOrReleaseDate.formattedInTimeZone(), forKey: .pickupOrReleaseDate)
try container.encode(isPickupDate, forKey: .isPickupDate)
try container.encode(pickupDescription ?? "", forKey: .pickupDescription)
try container.encode(publisher ?? "", forKey: .publisher)
try container.encode(isFinished, forKey: .isFinished)

View File

@@ -18,7 +18,8 @@ extension Game {
}
@NSManaged public var circumstances: String?
@NSManaged public var createdAt: Date
@NSManaged public var isPickupDate : Bool
@NSManaged public var pickupOrReleaseDate: Date
@NSManaged public var pickupDescription : String?
@NSManaged public var inWishlist: Bool
@NSManaged public var isDigital: Bool

View File

@@ -32,4 +32,34 @@ struct CDPreview {
}
}
private func games(predicate: NSPredicate?) -> [Game] {
let gameFR = NSFetchRequest<Game>(entityName: "Game")
if let predicate = predicate {
gameFR.predicate = predicate
}
do {
let games : [Game] = try CDManager.preview.viewContextPreview.fetch(gameFR)
return games
}catch {
return []
}
}
func games(console: Console) -> [Game] {
return games(predicate: NSPredicate(format: "console = %@", console))
}
func games() -> [Game] {
return games(predicate: .none)
}
func game(name: String) -> Game? {
return self.games().first(where: {$0.name == name})
}
func console(name: String) -> Console? {
return self.consoles().first(where: {$0.name == name})
}
}

View File

@@ -38,6 +38,13 @@ class ICloudManager {
return FileManager.default.fileExists(atPath: url.path)
}
#if DEBUG
let asset_name = path.replacingOccurrences(of: "/", with: "-")
if UIImage(named: asset_name) != nil {
return true
}
#endif
return false;
}
@@ -45,13 +52,31 @@ class ICloudManager {
guard let path = path else { return .none }
do {
let url = documents_folder?.appendingPathComponent(path)
let imageData = try Data(contentsOf: url!)
if let url = documents_folder?.appendingPathComponent(path) {
let imageData = try Data(contentsOf: url)
return UIImage(data: imageData)!
return UIImage(data:imageData)
}
}catch {
return .none
}
#if DEBUG
let asset_name = path.replacingOccurrences(of: "/", with: "-")
if let image = UIImage(named: asset_name) {
return image
}
#endif
return .none
}
static public func imageRatio(path: String?) -> CGFloat {
if let image = ICloudManager.imageFrom(path: path) {
return image.size.width / image.size.height
}
return 1
}
static public func inICloudContainer(url : URL?) -> Bool {

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "dreamcast.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "gamecube.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "nes.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "nintendo_switch.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ps2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ps3.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ps5.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "xbox_one.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ocarina_of_time.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "zelda_breath_of_the_wild.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -9,8 +9,11 @@
import Foundation
extension Date {
static let zockerhoehle_date_format = "yyyy-MM-dd'T'HH:mm:ssZ"
func formattedInTimeZone(timezone : TimeZone = .current) -> String {
static let export_date_format = "yyyy-MM-dd'T'HH:mm:ssZ"
static let view_date_format = "dd.MM.yyyy"
func formattedInTimeZone(timezone : TimeZone = .current, dateFormat : String = Date.export_date_format) -> String {
// 1) Create a DateFormatter() object.
let format = DateFormatter()
@@ -18,7 +21,7 @@ extension Date {
format.timeZone = timezone
// 3) Set the format of the altered date.
format.dateFormat = Date.zockerhoehle_date_format
format.dateFormat = dateFormat
// 4) Set the current date, altered by timezone.
let dateString = format.string(from: self)
@@ -29,7 +32,7 @@ extension Date {
static func from(string : String, timezone : TimeZone = .current) -> Date? {
let format = DateFormatter()
format.timeZone = timezone
format.dateFormat = Date.zockerhoehle_date_format
format.dateFormat = Date.export_date_format
return format.date(from: string)
}

View File

@@ -92,11 +92,13 @@ class LibraryImport {
cdGame.isDigital = game.isDigital
cdGame.playtime_h = game.playtime_h ?? 0
cdGame.playtime_min = game.playtime_min ?? 0
cdGame.isPickupDate = game.isPickupDate
if let date = Date.from(string: game.pickupOrReleaseDate) {
cdGame.pickupOrReleaseDate = date
if let date = Date.from(string: game.createdAt) {
cdGame.createdAt = date
}else{
print("Could not decode date '\(game.createdAt)' for game '\(cdGame.name)'")
print("Could not decode date '\(game.pickupOrReleaseDate)' for game '\(cdGame.name)'")
}
cdConsole.addToGames(cdGame)
@@ -218,7 +220,8 @@ struct BHLGame : Decodable {
let isFinished : Bool
let finishedDate : Date?
let notes : String?
let createdAt : String
let pickupOrReleaseDate : String
let isPickupDate : Bool = false
let pickupDescription : String?
let publisher : String?
let console : UUID

View File

@@ -116,7 +116,7 @@ struct ConsoleLibraryView : View {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(games.filter({$0.inWishlist == self.showWishlist})) { game in
NavigationLink(destination: GameDetailView(game: game)) {
GameView(game: game)
GameCover(game: game)
}
}
}.padding()

View File

@@ -0,0 +1,54 @@
//
// File.swift
// Zockerhoehle
//
// Created by Julian-Steffen Müller on 28.05.21.
// Copyright © 2021 Julian-Steffen Müller. All rights reserved.
//
import Foundation
import SwiftUI
struct ConsoleView : View {
@ObservedObject var console : Console
public var defaultImage = UIImage()
var body: some View {
ZStack {
Rectangle()
.fill(Color(red: 1, green: 0, blue: 0.03))
Image(uiImage: ICloudManager.imageFrom(path: console.logo_icloud_path) ?? defaultImage)
.resizable()
.padding()
.scaledToFit()
}
.padding(.horizontal, 5)
.aspectRatio(1.33, contentMode: .fit)
.cornerRadius(15)
}
}
struct ConsoleView_Previews: PreviewProvider {
static var previews: some View {
let consoles = CDPreview.shared.consoles()
let console_whish = CDPreview.shared.console(name: "Xbox One")
guard let console = console_whish ?? consoles.first else {
fatalError("No Console found in core data preview")
}
var view = ConsoleView(console: console)
view.defaultImage = UIImage(named: "PreviewConsole") ?? UIImage()
return Group {
view
view
}
}
}

View File

@@ -9,7 +9,7 @@
import Foundation
import SwiftUI
struct GameView : View {
struct GameCover : View {
@ObservedObject var game : Game
let defaultImage = UIImage()
@@ -34,3 +34,17 @@ struct GameView : View {
}
}
}
struct GameCover_Previews: PreviewProvider {
static var previews: some View {
let game_wish = CDPreview.shared.game(name: "Zelda: Breath of the Wild")
let games = CDPreview.shared.games()
guard let game = game_wish ?? games.first else {
fatalError("No game found in core data preview")
}
return GameCover(game: game)
}
}

View File

@@ -1,277 +1,151 @@
//
// GameDetailView.swift
// Zockerhoehle
//
// Created by Julian-Steffen Müller on 08.07.19.
// Copyright © 2019 Julian-Steffen Müller. All rights reserved.
//
import Foundation
import SwiftUI
struct GameSeriesPicker: View {
@ObservedObject var game : Game
@State private var selectedGameSeries = ""
@FetchRequest(entity: GameSeries.entity(), sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)])
var gameSeries: FetchedResults<GameSeries>
struct HeadlinedMultilineText : View {
let headline : String
let multilineText : String
var body: some View {
Picker("Spieleserie", selection: $selectedGameSeries) {
Text("Keine").tag("")
ForEach(gameSeries) { game_series in
Text(game_series.name ?? "n/a").tag(game_series.uuid.uuidString)
VStack(spacing: 0) {
HStack {
Text(headline)
.bold()
.font(.headline)
Spacer()
}
}.onChange(of: selectedGameSeries) { newValue in
if let game_series = gameSeries.first(where: {$0.uuid.uuidString == newValue}) {
if game.series != game_series {
game.series = game_series
}
Divider()
.padding(.bottom)
HStack{
Text(multilineText)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
init(game : Game) {
self.game = game
if let game_series = game.series {
_selectedGameSeries = State(initialValue: game_series.uuid.uuidString)
}
}
}
struct AttachmentCellImage : View {
@ObservedObject var game : Game
var onClick: ()->()
let defaultImage = UIImage()
struct InfoItem<Content : View> : View {
let headline : String
let content : () -> Content
var body: some View {
HStack{
Button("Cover") {
self.onClick()
}
VStack {
Text("Spielzeit")
.padding(.bottom)
.foregroundColor(.gray)
content()
}
}
}
Spacer()
struct GameView : View {
@ObservedObject var game : Game
Group {
Image(uiImage: ICloudManager.imageFrom(path: game.cover_icloud_path) ?? defaultImage)
.resizable()
.scaledToFit()
}.frame(width:100, height: 100)
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
private func coverPadding(game: Game) -> CGFloat {
if ICloudManager.imageRatio(path: game.cover_icloud_path) < 1 {
return 30
}else{
return 0
}
}
init(game: Game, onClick: @escaping ()->()) {
self.game = game
self.onClick = onClick
var body: some View {
ScrollView {
VStack {
GameCover(game: game)
.padding(.horizontal, coverPadding(game: game))
Text("\(game.name)")
.font(.title)
Text("\(game.console?.name ?? "No Console")")
.foregroundColor(.gray)
LazyVGrid(columns: columns) {
InfoItem(headline: "Gekauft") {
Text("\(game.pickupOrReleaseDate.formattedInTimeZone(dateFormat: Date.view_date_format))")
.bold()
}
if game.playtime_h > 0 || game.playtime_min > 0 {
InfoItem(headline: "Spielzeit") {
Text("\(game.playtime_h)h \(game.playtime_min)min")
}
}
if game.isFinished {
InfoItem(headline: "Durchgezockt") {
Text("Ja")
}
}
}
.padding(.vertical)
HeadlinedMultilineText(headline: "Notizen", multilineText: "Ganz toller multiline\nText")
.padding(.bottom)
HeadlinedMultilineText(headline: "Hinter den Kulissen", multilineText: "Ich habe für das Spiel bei MCMedia Games angestanden")
Spacer()
}
}.padding(.horizontal, 40)
}
}
struct GameView_Previews: PreviewProvider {
static var previews: some View {
let game_wish = CDPreview.shared.game(name: "Zelda - Ocarina of Time")
let games = CDPreview.shared.games()
guard let game = game_wish ?? games.first else {
fatalError("No game found in core data preview")
}
return GameView(game: game)
}
}
struct GameView_Landscape_Cover_Previews: PreviewProvider {
static var previews: some View {
let game_wish = CDPreview.shared.game(name: "Zelda: Breath of the Wild")
let games = CDPreview.shared.games()
guard let game = game_wish ?? games.first else {
fatalError("No game found in core data preview")
}
return GameView(game: game)
}
}
struct GameDetailView : View {
@ObservedObject var game : Game
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var showDeleteAlert : Bool = false
@State var isImportingCover : Bool = false
let defaultImage = UIImage()
@State var isLent_raw : Bool = false
private var isLent: Binding<Bool> {
Binding<Bool>(
get: { self.isLent_raw || (self.game.lentTo != .none && self.game.lentTo != "") },
set: {
isLent_raw = $0
if !$0 { self.game.lentTo = .none }
}
)
}
@State var hasFinishedDate_raw : Bool = false
private var hasFinishedDate: Binding<Bool> {
Binding<Bool>(
get: { self.hasFinishedDate_raw || self.game.finishedDate != .none },
set: {
hasFinishedDate_raw = $0
if !$0 { self.game.finishedDate = .none }
}
)
}
var finishedDateBinding: Binding<Date> {
Binding<Date>(
get: { self.game.finishedDate ?? Date() },
set: { self.game.finishedDate = $0 })
}
var notesBinding: Binding<String> {
Binding<String>(
get: { self.game.notes ?? "" },
set: { self.game.notes = $0 })
}
var pickupDscriptionBinding: Binding<String> {
Binding<String>(
get: { self.game.pickupDescription ?? "" },
set: { self.game.pickupDescription = $0 })
}
var lentToBinding: Binding<String> {
Binding<String>(
get: { self.game.lentTo ?? "" },
set: { self.game.lentTo = $0 })
}
@State var showWrongFolderAlert : Bool = false
var GameIsFinished : some View {
Group {
Toggle(isOn: $game.isFinished , label: {
Text("Durchgezockt")
}).onChange(of: game.isFinished) {
if !$0 {
game.playtime_h = 0
game.playtime_min = 0
}
}
if game.isFinished {
HStack {
Stepper("\(game.playtime_h)h", value: $game.playtime_h, in: 0...200)
Stepper("\(game.playtime_min)min", value: $game.playtime_min, in: 0...60)
}
Toggle(isOn: hasFinishedDate, label: {
Text("Gibts ein Datum")
})
}
if self.hasFinishedDate.wrappedValue && game.isFinished {
DatePicker("Durchgezockt am",
selection: finishedDateBinding,
in: ...Date(),
displayedComponents: [.date])
}
}
}
@State private var isInEditMode = false
var body: some View {
Form {
Section {
TextField("Videogame name", text: $game.name)
//Gray color should indicate immutable data
HStack {
Text("Konsole")
Text("\(game.console?.name ?? "No console")").foregroundColor(.gray)
}
Toggle(isOn: $game.isDigital, label: {
Text("Nur Digital")
})
Toggle(isOn: $game.inWishlist, label: {
Text("In Wunschliste")
})
}
Section(header: Text("Details")) {
DatePicker("In Sammlung seit",
selection: $game.createdAt,
in: ...Date(),
displayedComponents: [.date])
HStack {
Text("Anlass")
TextEditor(text: pickupDscriptionBinding).frame(height: 100)
}
GameSeriesPicker(game: game)
Toggle(isOn: isLent, label: {
Text("Verliehen?")
})
if isLent.wrappedValue {
TextField("Verliehen an", text: lentToBinding)
}
GameIsFinished
}
Section(header: Text("Notizen")) {
TextEditor(text: notesBinding).frame(height: /*@START_MENU_TOKEN@*/100/*@END_MENU_TOKEN@*/)
}
Section(header: Text("Anhänge")){
List {
AttachmentCellImage(game: game) {
self.isImportingCover = true
}
}
}
Section{
gameDeleteButton
}
}
.navigationBarTitle(Text("\(game.name)"), displayMode: .automatic)
.fileImporter(
isPresented: $isImportingCover,
allowedContentTypes: [.jpeg, .png],
allowsMultipleSelection: false
) { result in
do {
let selectedFile : URL = try result.get().first!
//It seems that isUbiquitousItem checks if the file is contained in the Apps iCloud folder
if (ICloudManager.inICloudContainer(url: selectedFile)) {
game.cover_icloud_path = ICloudManager.relativePathFrom(url: selectedFile)
print("Selected Image in iCloud Path \(game.cover_icloud_path ?? "n/a")")
}else{
self.showWrongFolderAlert = true
}
}catch{
print("ConsoleAllView::ModalAddConsoleToLibrary Error getting result '\(result)'")
Group {
if !isInEditMode {
GameView(game: game)
}else {
GameEditMode(game: game)
}
}
.alert(isPresented: $showWrongFolderAlert) {
Alert(
title: Text("Falscher Ordner"),
message: Text("Bitte nutze nur Dateien aus dem Zockerhöhle iCloud Ordner"),
dismissButton: .default(Text("Alles klar!"))
)
.navigationTitle("\(game.name)")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button(action: {
isInEditMode.toggle()
},
label: { Image(systemName: "plus")})
}
}
}
//Irgendwie gibt es einen Bug der den Alert im navigationBarItem zwei mal aufruft.
//Deswegen muss der delete Button is auf weiteres in die Form
var gameDeleteButton : some View {
Button(action: {
self.showDeleteAlert = true
}, label: {
Spacer()
Text("Spiel löschen")
Spacer()
//Image(systemName: "trash").foregroundColor(.red)
})
.accentColor(.red)
.alert(isPresented: $showDeleteAlert, content: {
Alert(title: Text("Aus Zockerhöhle entfernen"), message: Text("Willst du '\(self.game.name)' wirklich aus der Zockerhöhle werfen?"), primaryButton: Alert.Button.destructive(Text("Ja!"), action: {
//
self.presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
CDManager.shared.viewContext.delete(game)
}
}), secondaryButton: Alert.Button.cancel(Text("Lieber doch nicht")))
})
}
}

View File

@@ -0,0 +1,277 @@
//
// GameDetailView.swift
// Zockerhoehle
//
// Created by Julian-Steffen Müller on 08.07.19.
// Copyright © 2019 Julian-Steffen Müller. All rights reserved.
//
import SwiftUI
struct GameSeriesPicker: View {
@ObservedObject var game : Game
@State private var selectedGameSeries = ""
@FetchRequest(entity: GameSeries.entity(), sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)])
var gameSeries: FetchedResults<GameSeries>
var body: some View {
Picker("Spieleserie", selection: $selectedGameSeries) {
Text("Keine").tag("")
ForEach(gameSeries) { game_series in
Text(game_series.name ?? "n/a").tag(game_series.uuid.uuidString)
}
}.onChange(of: selectedGameSeries) { newValue in
if let game_series = gameSeries.first(where: {$0.uuid.uuidString == newValue}) {
if game.series != game_series {
game.series = game_series
}
}
}
}
init(game : Game) {
self.game = game
if let game_series = game.series {
_selectedGameSeries = State(initialValue: game_series.uuid.uuidString)
}
}
}
struct AttachmentCellImage : View {
@ObservedObject var game : Game
var onClick: ()->()
let defaultImage = UIImage()
var body: some View {
HStack{
Button("Cover") {
self.onClick()
}
Spacer()
Group {
Image(uiImage: ICloudManager.imageFrom(path: game.cover_icloud_path) ?? defaultImage)
.resizable()
.scaledToFit()
}.frame(width:100, height: 100)
}
}
init(game: Game, onClick: @escaping ()->()) {
self.game = game
self.onClick = onClick
}
}
struct GameEditMode : View {
@ObservedObject var game : Game
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var showDeleteAlert : Bool = false
@State var isImportingCover : Bool = false
let defaultImage = UIImage()
@State var isLent_raw : Bool = false
private var isLent: Binding<Bool> {
Binding<Bool>(
get: { self.isLent_raw || (self.game.lentTo != .none && self.game.lentTo != "") },
set: {
isLent_raw = $0
if !$0 { self.game.lentTo = .none }
}
)
}
@State var hasFinishedDate_raw : Bool = false
private var hasFinishedDate: Binding<Bool> {
Binding<Bool>(
get: { self.hasFinishedDate_raw || self.game.finishedDate != .none },
set: {
hasFinishedDate_raw = $0
if !$0 { self.game.finishedDate = .none }
}
)
}
var finishedDateBinding: Binding<Date> {
Binding<Date>(
get: { self.game.finishedDate ?? Date() },
set: { self.game.finishedDate = $0 })
}
var notesBinding: Binding<String> {
Binding<String>(
get: { self.game.notes ?? "" },
set: { self.game.notes = $0 })
}
var pickupDscriptionBinding: Binding<String> {
Binding<String>(
get: { self.game.pickupDescription ?? "" },
set: { self.game.pickupDescription = $0 })
}
var lentToBinding: Binding<String> {
Binding<String>(
get: { self.game.lentTo ?? "" },
set: { self.game.lentTo = $0 })
}
@State var showWrongFolderAlert : Bool = false
var GameIsFinished : some View {
Group {
Toggle(isOn: $game.isFinished , label: {
Text("Durchgezockt")
}).onChange(of: game.isFinished) {
if !$0 {
game.playtime_h = 0
game.playtime_min = 0
}
}
if game.isFinished {
HStack {
Stepper("\(game.playtime_h)h", value: $game.playtime_h, in: 0...200)
Stepper("\(game.playtime_min)min", value: $game.playtime_min, in: 0...60)
}
Toggle(isOn: hasFinishedDate, label: {
Text("Gibts ein Datum")
})
}
if self.hasFinishedDate.wrappedValue && game.isFinished {
DatePicker("Durchgezockt am",
selection: finishedDateBinding,
in: ...Date(),
displayedComponents: [.date])
}
}
}
var body: some View {
Form {
Section {
TextField("Videogame name", text: $game.name)
//Gray color should indicate immutable data
HStack {
Text("Konsole")
Text("\(game.console?.name ?? "No console")").foregroundColor(.gray)
}
Toggle(isOn: $game.isDigital, label: {
Text("Nur Digital")
})
Toggle(isOn: $game.inWishlist, label: {
Text("In Wunschliste")
})
}
Section(header: Text("Details")) {
DatePicker("In Sammlung seit",
selection: $game.pickupOrReleaseDate,
in: ...Date(),
displayedComponents: [.date])
HStack {
Text("Anlass")
TextEditor(text: pickupDscriptionBinding).frame(height: 100)
}
GameSeriesPicker(game: game)
Toggle(isOn: isLent, label: {
Text("Verliehen?")
})
if isLent.wrappedValue {
TextField("Verliehen an", text: lentToBinding)
}
GameIsFinished
}
Section(header: Text("Notizen")) {
TextEditor(text: notesBinding).frame(height: /*@START_MENU_TOKEN@*/100/*@END_MENU_TOKEN@*/)
}
Section(header: Text("Anhänge")){
List {
AttachmentCellImage(game: game) {
self.isImportingCover = true
}
}
}
Section{
gameDeleteButton
}
}
.navigationBarTitle(Text("\(game.name)"), displayMode: .automatic)
.fileImporter(
isPresented: $isImportingCover,
allowedContentTypes: [.jpeg, .png],
allowsMultipleSelection: false
) { result in
do {
let selectedFile : URL = try result.get().first!
//It seems that isUbiquitousItem checks if the file is contained in the Apps iCloud folder
if (ICloudManager.inICloudContainer(url: selectedFile)) {
game.cover_icloud_path = ICloudManager.relativePathFrom(url: selectedFile)
print("Selected Image in iCloud Path \(game.cover_icloud_path ?? "n/a")")
}else{
self.showWrongFolderAlert = true
}
}catch{
print("ConsoleAllView::ModalAddConsoleToLibrary Error getting result '\(result)'")
}
}
.alert(isPresented: $showWrongFolderAlert) {
Alert(
title: Text("Falscher Ordner"),
message: Text("Bitte nutze nur Dateien aus dem Zockerhöhle iCloud Ordner"),
dismissButton: .default(Text("Alles klar!"))
)
}
}
//Irgendwie gibt es einen Bug der den Alert im navigationBarItem zwei mal aufruft.
//Deswegen muss der delete Button is auf weiteres in die Form
var gameDeleteButton : some View {
Button(action: {
self.showDeleteAlert = true
}, label: {
Spacer()
Text("Spiel löschen")
Spacer()
//Image(systemName: "trash").foregroundColor(.red)
})
.accentColor(.red)
.alert(isPresented: $showDeleteAlert, content: {
Alert(title: Text("Aus Zockerhöhle entfernen"), message: Text("Willst du '\(self.game.name)' wirklich aus der Zockerhöhle werfen?"), primaryButton: Alert.Button.destructive(Text("Ja!"), action: {
//
self.presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
CDManager.shared.viewContext.delete(game)
}
}), secondaryButton: Alert.Button.cancel(Text("Lieber doch nicht")))
})
}
}

View File

@@ -16,7 +16,7 @@ struct GamePickupsView: View {
init() {
let gamesFR = NSFetchRequest<Game>(entityName: "Game")
gamesFR.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false), NSSortDescriptor(key: "name", ascending: true)]
gamesFR.sortDescriptors = [NSSortDescriptor(key: "pickupOrReleaseDate", ascending: false), NSSortDescriptor(key: "name", ascending: true)]
gamesFR.fetchLimit = 50;
_games = FetchRequest(fetchRequest: gamesFR)
}
@@ -29,8 +29,8 @@ struct GamePickupsView: View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(games) { game in
NavigationLink(destination: GameDetailView(game: game)) {
GameView(game: game)
NavigationLink(destination: GameEditMode(game: game)) {
GameCover(game: game)
}
}
}.padding()

View File

@@ -35,8 +35,8 @@ struct GameSeriesLibraryView: View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(games) { game in
NavigationLink(destination: GameDetailView(game: game)) {
GameView(game: game)
NavigationLink(destination: GameEditMode(game: game)) {
GameCover(game: game)
}
}
}.padding()

View File

@@ -54,7 +54,7 @@ struct EmptyCategory : View {
struct Overview: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(entity: Game.entity(), sortDescriptors: [NSSortDescriptor(key: "createdAt", ascending: false), NSSortDescriptor(key: "name", ascending: true)])
@FetchRequest(entity: Game.entity(), sortDescriptors: [NSSortDescriptor(key: "pickupOrReleaseDate", ascending: false), NSSortDescriptor(key: "name", ascending: true)])
var games: FetchedResults<Game>
@FetchRequest(entity: Game.entity(), sortDescriptors: [NSSortDescriptor(key: "finishedDate", ascending: false), NSSortDescriptor(key: "name", ascending: true)])
@@ -115,10 +115,10 @@ struct Overview: View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(games.prefix(last_picksups_limit)) { game in
NavigationLink(destination: GameDetailView(game: game)) {
NavigationLink(destination: GameEditMode(game: game)) {
VStack(alignment: .leading) {
Group {
GameView(game: game)
GameCover(game: game)
}
.frame(width: 100, height: 100)
@@ -145,10 +145,10 @@ struct Overview: View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(gamesFinished.prefix(last_picksups_limit).filter({$0.isFinished})) { game in
NavigationLink(destination: GameDetailView(game: game)) {
NavigationLink(destination: GameEditMode(game: game)) {
VStack(alignment: .leading) {
Group {
GameView(game: game)
GameCover(game: game)
}
.frame(width: 100, height: 100)

View File

@@ -17,5 +17,9 @@
<array>
<string>iCloud.Zockerhoehle</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -23,15 +23,16 @@
</entity>
<entity name="Game" representedClassName="Game" syncable="YES">
<attribute name="cover_icloud_path" optional="YES" attributeType="String"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="finishedDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="inWishlist" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isDigital" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isFinished" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isPickupDate" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="lentTo" optional="YES" attributeType="String"/>
<attribute name="name" attributeType="String" defaultValueString=""/>
<attribute name="notes" optional="YES" attributeType="String"/>
<attribute name="pickupDescription" optional="YES" attributeType="String"/>
<attribute name="pickupOrReleaseDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="playtime_h" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="playtime_min" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="publisher" optional="YES" attributeType="String"/>
@@ -54,7 +55,7 @@
<elements>
<element name="Accessory" positionX="-265.9140625" positionY="29.15625" width="128" height="149"/>
<element name="Console" positionX="-535.7890625" positionY="56.03515625" width="128" height="164"/>
<element name="Game" positionX="-288.828125" positionY="276.7421875" width="128" height="290"/>
<element name="Game" positionX="-288.828125" positionY="276.7421875" width="128" height="305"/>
<element name="GameSeries" positionX="-686.828125" positionY="359.20703125" width="128" height="89"/>
<element name="GameSeriesCover" positionX="-477" positionY="180" width="128" height="44"/>
</elements>