Merge pull request #60 from YusukeHosonuma/cross-platform/render-thumbnail

This commit is contained in:
Yusuke Hosonuma
2020-08-13 18:29:12 +09:00
committed by GitHub
9 changed files with 121 additions and 73 deletions
+18 -4
View File
@@ -57,7 +57,6 @@
E55F7AA624DD0C1000DBC175 /* MacRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55F7AA524DD0C1000DBC175 /* MacRootView.swift */; };
E55F7AA824DD0D9300DBC175 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55F7AA724DD0D9300DBC175 /* ContentView.swift */; };
E55F7AAA24DD101900DBC175 /* PresetListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55F7AA924DD101900DBC175 /* PresetListView.swift */; };
E55F7AAC24DD119700DBC175 /* BoardThumnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55F7AAB24DD119700DBC175 /* BoardThumnailView.swift */; };
E5604AA124CC3ED600EA59C3 /* PreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5604AA024CC3ED600EA59C3 /* PreferenceView.swift */; };
E56A422824E2BB6800F9AC24 /* BoardDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52FE5DC24DC012A00D93764 /* BoardDocument.swift */; };
E56A422924E2C0B100F9AC24 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = E558A97224DB70D00012E443 /* GoogleService-Info.plist */; };
@@ -98,6 +97,13 @@
E5D9F27424BEBDBD009A526B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D9F24A24BEBDBC009A526B /* Application.swift */; };
E5D9F27524BEBDBD009A526B /* MainGameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D9F24B24BEBDBC009A526B /* MainGameView.swift */; };
E5DB4C9524D853C000AADA66 /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5DB4C9424D853C000AADA66 /* PhotoPicker.swift */; };
E5E50A2524E52C5300CD64B6 /* GraphicsImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E50A2424E52C5300CD64B6 /* GraphicsImageRenderer.swift */; };
E5E50A2624E52EE000CD64B6 /* BoardThumbnailImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E6EB1524DFF33E0012E911 /* BoardThumbnailImage.swift */; };
E5E50A2724E5327E00CD64B6 /* GraphicsImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E50A2424E52C5300CD64B6 /* GraphicsImageRenderer.swift */; };
E5E50A2824E535AD00CD64B6 /* GraphicsImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E50A2424E52C5300CD64B6 /* GraphicsImageRenderer.swift */; };
E5E50A2A24E539C900CD64B6 /* Image+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E50A2924E539C900CD64B6 /* Image+.swift */; };
E5E50A2B24E539C900CD64B6 /* Image+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E50A2924E539C900CD64B6 /* Image+.swift */; };
E5E50A2C24E539C900CD64B6 /* Image+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E50A2924E539C900CD64B6 /* Image+.swift */; };
E5E6EB1424DFF27C0012E911 /* BoardSelectCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E6EB1324DFF27C0012E911 /* BoardSelectCell.swift */; };
E5E6EB1624DFF33E0012E911 /* BoardThumbnailImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E6EB1524DFF33E0012E911 /* BoardThumbnailImage.swift */; };
E5FA934224C0570800229AAA /* ButtonStyleCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5FA934124C0570800229AAA /* ButtonStyleCircle.swift */; };
@@ -186,7 +192,6 @@
E55F7AA524DD0C1000DBC175 /* MacRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacRootView.swift; sourceTree = "<group>"; };
E55F7AA724DD0D9300DBC175 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
E55F7AA924DD101900DBC175 /* PresetListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetListView.swift; sourceTree = "<group>"; };
E55F7AAB24DD119700DBC175 /* BoardThumnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardThumnailView.swift; sourceTree = "<group>"; };
E5604AA024CC3ED600EA59C3 /* PreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceView.swift; sourceTree = "<group>"; };
E56DBCAB24E4ED5000849CE4 /* LifeGameConfig.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = LifeGameConfig.intentdefinition; sourceTree = "<group>"; };
E56DBCAE24E4FE3200849CE4 /* Array+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+.swift"; sourceTree = "<group>"; };
@@ -219,6 +224,8 @@
E5D9F27024BEBDBD009A526B /* Tests_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOS.swift; sourceTree = "<group>"; };
E5D9F27224BEBDBD009A526B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E5DB4C9424D853C000AADA66 /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = "<group>"; };
E5E50A2424E52C5300CD64B6 /* GraphicsImageRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicsImageRenderer.swift; sourceTree = "<group>"; };
E5E50A2924E539C900CD64B6 /* Image+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image+.swift"; sourceTree = "<group>"; };
E5E6EB1324DFF27C0012E911 /* BoardSelectCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardSelectCell.swift; sourceTree = "<group>"; };
E5E6EB1524DFF33E0012E911 /* BoardThumbnailImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardThumbnailImage.swift; sourceTree = "<group>"; };
E5FA934124C0570800229AAA /* ButtonStyleCircle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyleCircle.swift; sourceTree = "<group>"; };
@@ -377,6 +384,7 @@
E571536424D51B5B003B4C9E /* Util */ = {
isa = PBXGroup;
children = (
E5E50A2424E52C5300CD64B6 /* GraphicsImageRenderer.swift */,
E571536524D51B73003B4C9E /* UserDefaultConvertible.swift */,
E571536824D51B9F003B4C9E /* UserDefault.swift */,
E518C88E24E3670C00B02027 /* UserDefaultGroup.swift */,
@@ -460,7 +468,6 @@
E55F7AA524DD0C1000DBC175 /* MacRootView.swift */,
E55F7AA724DD0D9300DBC175 /* ContentView.swift */,
E55F7AA924DD101900DBC175 /* PresetListView.swift */,
E55F7AAB24DD119700DBC175 /* BoardThumnailView.swift */,
);
path = View;
sourceTree = "<group>";
@@ -624,6 +631,7 @@
E5FA934F24C07AE300229AAA /* Collection+.swift */,
E518213224CC2E5300ABD16B /* Color+.swift */,
E56DBCAE24E4FE3200849CE4 /* Array+.swift */,
E5E50A2924E539C900CD64B6 /* Image+.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -952,6 +960,7 @@
E5B7516124E3D7A00004D8AE /* FirestoreBoardRepository.swift in Sources */,
E56DBCB324E4FFF400849CE4 /* Array+.swift in Sources */,
E5438E3A24E2AB0B007A3EB7 /* LifeGameWidget.swift in Sources */,
E5E50A2824E535AD00CD64B6 /* GraphicsImageRenderer.swift in Sources */,
E518C89824E3690600B02027 /* Color+.swift in Sources */,
E56DBCAD24E4ED5000849CE4 /* LifeGameConfig.intentdefinition in Sources */,
E56A422824E2BB6800F9AC24 /* BoardDocument.swift in Sources */,
@@ -959,6 +968,7 @@
E518C89624E368A700B02027 /* UserDefaultGroup.swift in Sources */,
E5438E4724E2AD1D007A3EB7 /* BoardPreset.swift in Sources */,
E5438E4524E2ACD4007A3EB7 /* BoardThumbnailImage.swift in Sources */,
E5E50A2C24E539C900CD64B6 /* Image+.swift in Sources */,
E518C89724E368D600B02027 /* UserDefaultConvertible.swift in Sources */,
E518C89524E3684A00B02027 /* UserDefaultSettingGroup.swift in Sources */,
);
@@ -997,6 +1007,7 @@
E547C30A24E3F4CF0022DF3A /* MemoryStorage.swift in Sources */,
E5D9F27324BEBDBD009A526B /* Application.swift in Sources */,
E5B0572824CC3465005EE1AA /* CellView.swift in Sources */,
E5E50A2724E5327E00CD64B6 /* GraphicsImageRenderer.swift in Sources */,
E5FA935024C07AE300229AAA /* Collection+.swift in Sources */,
E518C89324E3684A00B02027 /* UserDefaultSettingGroup.swift in Sources */,
E5B7519B24E3DD6D0004D8AE /* DesigntimeFirestoreBoardRepository.swift in Sources */,
@@ -1004,6 +1015,7 @@
E51169D224D644E3002D40B6 /* SpeedSliderView.swift in Sources */,
E5DB4C9524D853C000AADA66 /* PhotoPicker.swift in Sources */,
E5FA934224C0570800229AAA /* ButtonStyleCircle.swift in Sources */,
E5E50A2A24E539C900CD64B6 /* Image+.swift in Sources */,
E54904AF24DD5E2D003AE7C7 /* BoardView.swift in Sources */,
E5CE237324C199F200BC5820 /* LifeGameContext.swift in Sources */,
);
@@ -1036,14 +1048,16 @@
E5B7519C24E3DD6D0004D8AE /* DesigntimeFirestoreBoardRepository.swift in Sources */,
E5B0572924CC3465005EE1AA /* CellView.swift in Sources */,
E5FA935124C07AE300229AAA /* Collection+.swift in Sources */,
E55F7AAC24DD119700DBC175 /* BoardThumnailView.swift in Sources */,
E5FA934324C0570800229AAA /* ButtonStyleCircle.swift in Sources */,
E580AA6D24E12F34007C60F1 /* BoardPresetFile.swift in Sources */,
E571536A24D51B9F003B4C9E /* UserDefault.swift in Sources */,
E5CE237424C199F200BC5820 /* LifeGameContext.swift in Sources */,
E5E50A2624E52EE000CD64B6 /* BoardThumbnailImage.swift in Sources */,
E5E50A2524E52C5300CD64B6 /* GraphicsImageRenderer.swift in Sources */,
E55F7AA824DD0D9300DBC175 /* ContentView.swift in Sources */,
E547C30B24E3F4CF0022DF3A /* MemoryStorage.swift in Sources */,
E5604AA124CC3ED600EA59C3 /* PreferenceView.swift in Sources */,
E5E50A2B24E539C900CD64B6 /* Image+.swift in Sources */,
E573BD8224D053F90096BF75 /* LifeGameCommands.swift in Sources */,
E518C89424E3684A00B02027 /* UserDefaultSettingGroup.swift in Sources */,
);
+10
View File
@@ -7,6 +7,16 @@
import SwiftUI
extension Color {
var cgColor: CGColor {
#if os(macOS)
return NSColor(self).cgColor
#else
return UIColor(self).cgColor
#endif
}
}
extension Color: RawRepresentable {
public var rawValue: Data {
#if os(macOS)
+20
View File
@@ -0,0 +1,20 @@
//
// Image+.swift
// LifeGameApp
//
// Created by Yusuke Hosonuma on 2020/08/13.
//
import SwiftUI
extension Image {
#if os(macOS)
init(image: NSImage) {
self = Image(nsImage: image)
}
#else
init(image: UIImage) {
self = Image(uiImage: image)
}
#endif
}
+5 -8
View File
@@ -1,23 +1,20 @@
//
// MemoryStorage.swift
// MemoryCacheStorage.swift
// LifeGameApp
//
// Created by Yusuke Hosonuma on 2020/08/12.
//
import Foundation
import UIKit
final class ThumbnailImageCacheStorage {
static var shared = ThumbnailImageCacheStorage()
final class MemoryCacheStorage<T: AnyObject> {
private let cache = NSCache<NSString, T>()
private let cache = NSCache<NSString, UIImage>()
func store(key: String, image: UIImage) {
func store(key: String, image: T) {
cache.setObject(image, forKey: key as NSString)
}
func value(forKey key: String) -> UIImage? {
func value(forKey key: String) -> T? {
guard let value = cache.object(forKey: key as NSString) else { return nil }
return value
}
+36
View File
@@ -0,0 +1,36 @@
//
// GraphicsImageRenderer.swift
// LifeGameApp (macOS)
//
// Created by Yusuke Hosonuma on 2020/08/13.
//
#if os(macOS)
import AppKit
#else
import UIKit
#endif
final class GraphicsImageRenderer {
let size: CGSize
init(size: CGSize) {
self.size = size
}
#if os(macOS)
func image(actions: (CGContext) -> ()) -> NSImage {
let image = NSImage(size: size)
image.lockFocusFlipped(true)
actions(NSGraphicsContext.current!.cgContext)
image.unlockFocus()
return image
}
#else
func image(actions: @escaping (CGContext) -> ()) -> UIImage {
UIGraphicsImageRenderer(size: size).image { context in
actions(context.cgContext)
}
}
#endif
}
+2 -2
View File
@@ -18,7 +18,7 @@ struct BoardSelectCell: View {
switch style {
case .grid:
VStack {
BoardThumbnailImage(board: item.makeBoard().extended(by: .die), cacheKey: item.id)
BoardThumbnailImage(board: item.makeBoard(), cacheKey: item.id)
.matchedGeometryEffect(id: "thumbnail-\(item.title)", in: nspace)
HStack {
@@ -54,7 +54,7 @@ struct BoardSelectCell: View {
.foregroundColor(.yellow)
}
Spacer()
BoardThumbnailImage(board: item.makeBoard().extended(by: .die), cacheKey: item.id)
BoardThumbnailImage(board: item.makeBoard(), cacheKey: item.id)
.frame(width: 60, height: 60, alignment: .center)
.matchedGeometryEffect(id: "thumbnail-\(item.title)", in: nspace)
}.padding()
+28 -16
View File
@@ -8,17 +8,29 @@
import SwiftUI
import LifeGame
#if os(macOS)
private typealias XImage = NSImage
#else
private typealias XImage = UIImage
#endif
private let cacheStorage = MemoryCacheStorage<XImage>()
struct BoardThumbnailImage: View {
@Environment(\.colorScheme) var colorScheme
var board: Board<Cell>
var cellColor: Color?
var cacheKey: String?
private let board: Board<Cell>
private let cellColor: Color?
private let cacheKey: String?
private let cacheStorage = ThumbnailImageCacheStorage.shared
init(board: Board<Cell>, cellColor: Color? = nil, cacheKey: String? = nil) {
self.board = board.extended(by: .die)
self.cellColor = cellColor
self.cacheKey = cacheKey
}
var body: some View {
Image(uiImage: thumbnailImage)
Image(image: thumbnailImage)
.antialiased(false)
.resizable()
.scaledToFit()
@@ -26,19 +38,19 @@ struct BoardThumbnailImage: View {
private var fillColor: CGColor {
if let color = cellColor {
return UIColor(color).cgColor
return color.cgColor
} else {
return colorScheme == .dark
? UIColor(Color.white).cgColor
: UIColor(Color.black).cgColor
? Color.white.cgColor
: Color.black.cgColor
}
}
private var gridColor: CGColor {
UIColor(Color.gray.opacity(0.3)).cgColor
Color.gray.opacity(0.3).cgColor
}
private var thumbnailImage: UIImage {
private var thumbnailImage: XImage {
guard let cacheKey = cacheKey else {
return renderImage()
}
@@ -53,14 +65,14 @@ struct BoardThumbnailImage: View {
return image
}
}
private func renderImage() -> UIImage {
private func renderImage() -> XImage {
let scale = max(2, 140 / board.size)
let size = CGSize(width: board.size * scale + 1, height: board.size * scale + 1)
return UIGraphicsImageRenderer(size: size)
return GraphicsImageRenderer(size: size)
.image(actions: { context in
context.cgContext.setFillColor(fillColor)
context.setFillColor(fillColor)
for (index, cell) in board.cells.enumerated() {
let x = (index % board.size) * scale
@@ -71,7 +83,7 @@ struct BoardThumbnailImage: View {
}
// Draw grid
context.cgContext.setFillColor(gridColor)
context.setFillColor(gridColor)
for index in 0...board.size + 1 {
let length = board.size * scale + 1
context.fill(CGRect(x: scale * index, y: 0, width: 1, height: length)) // vertical lines
-42
View File
@@ -1,42 +0,0 @@
//
// BoardThumnailView.swift
// LifeGameApp (macOS)
//
// Created by Yusuke Hosonuma on 2020/08/07.
//
import SwiftUI
import LifeGame
struct BoardThumnailView: View {
@Environment(\.colorScheme) private var colorScheme: ColorScheme
var board: LifeGameBoard
var body: some View {
VStack(spacing: 0) {
ForEach(board.rows.withIndex(), id: \.0) { y, row in
HStack(spacing: 0) {
ForEach(row.withIndex(), id: \.0) { x, cell in
CellView(color: cellColor(cell), size: 100.0 / CGFloat(board.size)) // TODO:
}
}
}
}
}
private func cellColor(_ cell: Cell) -> Color {
switch (cell, colorScheme) {
case (.die, _): return .clear
case (.alive, .light): return .black
case (.alive, .dark): return .white
@unknown default: fatalError()
}
}
}
struct BoardThumnailView_Previews: PreviewProvider {
static var previews: some View {
BoardThumnailView(board: BoardPreset.nebura.board)
}
}
+2 -1
View File
@@ -17,7 +17,8 @@ struct PresetListView: View {
Section(header: Text("Presets")) {
ForEach(boardRepository.items) { item in
HStack {
BoardThumnailView(board: LifeGameBoard(board: item.makeBoard()))
BoardThumbnailImage(board: item.makeBoard(), cacheKey: item.id)
.frame(width: 120, height: 120, alignment: .center)
Text("\(item.title)")
}
.contentShape(Rectangle()) // not working in beta4