Created
September 4, 2021 13:12
-
-
Save codelynx/f8c4d8f394ee64483cd05e60332253e2 to your computer and use it in GitHub Desktop.
This code demonstrate archive and unarchive objects derived from a certain class and its descendants using Codable and Runtime Utility
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
This sample code shows the problem of archive and unarchive abstruct objects. Please see `TODO` keyword | |
and make this Contents class to save and load shape objects. | |
CASE: All target objects are based on NSObject with NSCoding to archive and unarchive Shape desendant class objects | |
file: archive-unarchive-1.swift | |
https://gist.github.com/codelynx/428b27b3cfd58b8c7382346f1a4bc415 | |
*/ | |
import Foundation | |
import CoreGraphics | |
// Utility class to inspect runtime classes | |
public class Runtime { | |
public static func allClasses() -> [AnyClass] { | |
let numberOfClasses = Int(objc_getClassList(nil, 0)) | |
if numberOfClasses > 0 { | |
let classesPtr = UnsafeMutablePointer<AnyClass>.allocate(capacity: numberOfClasses) | |
let autoreleasingClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(classesPtr) | |
let count = objc_getClassList(autoreleasingClasses, Int32(numberOfClasses)) | |
assert(numberOfClasses == count) | |
defer { classesPtr.deallocate() } | |
let classes = (0 ..< numberOfClasses).map { classesPtr[$0] } | |
return classes | |
} | |
return [] | |
} | |
public static func subclasses(of `class`: AnyClass) -> [AnyClass] { | |
return self.allClasses().filter { | |
var ancestor: AnyClass? = $0 | |
while let type = ancestor { | |
if ObjectIdentifier(type) == ObjectIdentifier(`class`) { return true } | |
ancestor = class_getSuperclass(type) | |
} | |
return false | |
} | |
} | |
public static func classes(conformToProtocol `protocol`: Protocol) -> [AnyClass] { | |
let classes = self.allClasses().filter { `class` in | |
var subject: AnyClass? = `class` | |
while let `class` = subject { | |
if class_conformsToProtocol(`class`, `protocol`) { print(String(describing: `class`)); return true } | |
subject = class_getSuperclass(`class`) | |
} | |
return false | |
} | |
return classes | |
} | |
public static func classes<T>(conformTo: T.Type) -> [AnyClass] { | |
return self.allClasses().filter { $0 is T } | |
} | |
} | |
// Main code | |
protocol Shape: Codable, CustomStringConvertible { | |
init(from decoder: Decoder) throws | |
func encode(to encoder: Encoder) throws | |
} | |
class Circle: Shape { | |
private enum CodingKeys: String, CodingKey { case center, radius } | |
var center: CGPoint | |
var radius: CGFloat | |
init(center: CGPoint, radius: CGFloat) { | |
self.center = center | |
self.radius = radius | |
} | |
required init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
self.center = try container.decode(CGPoint.self, forKey: .center) | |
self.radius = try container.decode(CGFloat.self, forKey: .radius) | |
} | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.container(keyedBy: CodingKeys.self) | |
try container.encode(self.center, forKey: .center) | |
try container.encode(self.radius, forKey: .radius) | |
} | |
var description: String { "{Circle: center=\(self.center), radius=\(self.radius)}" } | |
} | |
class Rectangle: Shape { | |
private enum CodingKeys: String, CodingKey { case origin, size } | |
var origin: CGPoint | |
var size: CGSize | |
init(origin: CGPoint, size: CGSize) { | |
self.origin = origin | |
self.size = size | |
} | |
required init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
self.origin = try container.decode(CGPoint.self, forKey: .origin) | |
self.size = try container.decode(CGSize.self, forKey: .size) | |
} | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.container(keyedBy: CodingKeys.self) | |
try container.encode(self.origin, forKey: .origin) | |
try container.encode(self.size, forKey: .size) | |
} | |
var description: String { "{Rectangle: origin=\(self.origin), size=\(self.size)}" } | |
} | |
class RoundedRectangle: Rectangle { | |
private enum CodingKeys: String, CodingKey { case cornerRadius } | |
var cornerRadius: CGFloat | |
init(origin: CGPoint, size: CGSize, cornerRadius: CGFloat) { | |
self.cornerRadius = cornerRadius | |
super.init(origin: origin, size: size) | |
} | |
required init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
self.cornerRadius = try container.decode(CGFloat.self, forKey: .cornerRadius) | |
try super.init(from: decoder) | |
} | |
override func encode(to encoder: Encoder) throws { | |
var container = encoder.container(keyedBy: CodingKeys.self) | |
try container.encode(self.cornerRadius, forKey: .cornerRadius) | |
try super.encode(to: encoder) | |
} | |
override var description: String { "{RoundedRectangle: \(super.description), cornerRadius=\(self.cornerRadius)}" } | |
} | |
class Contents: Codable { | |
private enum CodingKeys: String, CodingKey { case shapes, type, shape } | |
var shapes: [Shape] | |
init(shapes: [Shape]) { | |
self.shapes = shapes | |
} | |
required init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
var subcontainer = try container.nestedUnkeyedContainer(forKey: .shapes) | |
let classes = Runtime.classes(conformTo: Shape.Type.self) | |
var shapes = [Shape]() | |
while (!subcontainer.isAtEnd) { | |
let type = try subcontainer.decode(String.self) | |
if let shapeType = classes.filter({ "\($0.self)" == type }).first as? Shape.Type { | |
let shape = try shapeType.init(from: subcontainer.superDecoder()) | |
shapes.append(shape) | |
} | |
} | |
self.shapes = shapes | |
} | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.container(keyedBy: CodingKeys.self) | |
var subcontainer = container.nestedUnkeyedContainer(forKey: .shapes) | |
for shape in self.shapes { | |
let typeString: String = "\(type(of: shape))" | |
try subcontainer.encode(typeString) | |
try shape.encode(to: subcontainer.superEncoder()) | |
} | |
} | |
} | |
func test() throws { | |
let a = Circle(center: CGPoint(x: 10, y: 20), radius: 30) | |
let b = Rectangle(origin: CGPoint(x: 40, y: 50), size: CGSize(width: 60, height: 70)) | |
let c = RoundedRectangle(origin: CGPoint(x: 100, y: 110), size: CGSize(width: 120, height: 130), cornerRadius: 5) | |
let contents = Contents(shapes: [a, b, c]) | |
let encoder = JSONEncoder() | |
let data: Data = try encoder.encode(contents) | |
print("encoded:", data as NSData) | |
let decoder = JSONDecoder() | |
let object = try decoder.decode(Contents.self, from: data) | |
print(object.shapes.map { $0.description }.joined(separator: "\r")) | |
} | |
// CAUTION: This code may not work on Playground. | |
do { try test() } | |
catch { print("\(error)") } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment