Skip to content

Instantly share code, notes, and snippets.

@yosshi4486
Last active April 8, 2023 06:23
Show Gist options
  • Save yosshi4486/852376ccb0fc934b5f3288ff5d7f3799 to your computer and use it in GitHub Desktop.
Save yosshi4486/852376ccb0fc934b5f3288ff5d7f3799 to your computer and use it in GitHub Desktop.
Swift implementation of a `BackForwardList` which can manage back and forward.
//
// BackForwardList.swift
//
// Created by yosshi4486 on 2023/04/02.
//
import Foundation
/// 進む・戻る管理の可能なリスト
///
/// WKBackForwardListにインスパイアされて実装
/// https://developer.apple.com/documentation/webkit/wkbackforwardlist
struct BackForwardList<Item>: Collection where Item: Hashable {
/// 内部ストレージ
private var internalStorage: Array<Item> = .init()
/// 続いて同じアイテムを保持することを許容するかどうかのBool値.
///
/// この値が`false`のとき、`add(_:)`で`currentItem`と同じハッシュ値の要素を渡すと、その要素は追加されない
var allowsConsecutiveItems: Bool = true
/// `currentItem`のリスト内でのindex. アイテムが存在しない場合は-1を返し、存在する場合は0以上で1づつ増加して返す
private(set) var currentItemIndex: Int = -1
/// リスト内の`currentItemIndex`が指す要素. データが空の場合`nil`を返す
var currentItem: Item? {
guard !internalStorage.isEmpty else {
return nil
}
return internalStorage[currentItemIndex]
}
/// 現在のアイテムより1つ前にあるアイテム
var backItem: Item? { backItems.first }
/// 現在のアイテムより1つ次にあるアイテム
var forwardItem: Item? { forwardItems.first }
/// 現在のアイテムより前にあるアイテムの配列. 先頭ほど`currentItem`に近く、末尾ほど`currenItem`から遠いアイテムを指す
///
/// このリスト内のアイテムの`currentItemIndex`より前の要素を返す. データが空、`canBack`が`false`の場合は空配列を返す
var backItems: [Item] {
guard !internalStorage.isEmpty, canBack else {
return []
}
return Array(internalStorage[startIndex..<currentItemIndex]).reversed()
}
/// 現在のアイテムより後ろにあるアイテムの配列. 先頭ほど`currentItem`に近く、末尾ほど`currenItem`から遠いアイテムを指す
///
/// このリスト内のアイテムの`currentItemIndex`より後の要素を返す. データが空、`canForward`が`false`の場合は空配列を返す.
var forwardItems: [Item] {
guard !internalStorage.isEmpty, canForward else {
return []
}
return Array(internalStorage[(currentItemIndex + 1)..<endIndex])
}
var startIndex: Int {
return internalStorage.startIndex
}
var endIndex: Int {
return internalStorage.endIndex
}
/// 履歴内の1つ前のアイテムに戻れるかどうかのBool値. 戻れれば`true`、そうでなければ`false`.
var canBack: Bool {
return (currentItemIndex - 1) >= startIndex
}
/// 履歴内の1つ次のアイテムに進めるかどうかのBool値. 進めればば`true`、そうでなければ`false`.
var canForward: Bool {
return (currentItemIndex + 1) < endIndex
}
/// 空の`BackForwardList`を初期化する
init() {
self.internalStorage = []
}
/// 与えられた`items`でリストを初期化する
///
/// - Parameter items: BackForwardListのHashableな要素.
init(items: [Item]) {
self.internalStorage = items
self.currentItemIndex = items.endIndex - 1
}
subscript(position: Int) -> Item {
return internalStorage[position]
}
func index(after i: Int) -> Int {
return internalStorage.index(after: i)
}
/// 指定した`item`をリストに追加する. `allowsConsecutiveItems`が`true`の場合、`currentItem`と同じアイテムを渡してもリストに追加されるが、そうでない場合は無視され追加されない。
///
/// - Parameter item: 新規追加するアイテム.
mutating func add(_ item: Item) {
if allowsConsecutiveItems == false && item == currentItem {
return
}
var arraySlice = internalStorage.prefix(through: currentItemIndex)
arraySlice.append(item)
internalStorage = Array(arraySlice)
currentItemIndex = internalStorage.endIndex - 1
}
/// `canBack`を満たす場合のみ、リストの現在指定位置を1つ戻す.
mutating func back() {
guard canBack else { return }
currentItemIndex -= 1
}
/// `canForward`を満たす場合のみ、リストの現在指定位置を1つ進める.
mutating func forward() {
guard canForward else { return }
currentItemIndex += 1
}
/// 与えられた`item`を`currentItem`に設定する.
///
/// このリスト内の要素を`item`に渡した場合、`currentItem`がその`item`を指すようになり、`true`を返す。
/// このリスト内に存在しない要素を`item`に渡した場合、`false`を返す.
///
/// - Parameter item: `currentItem`に設定したいアイテム.
@discardableResult mutating func makeCurrentItem(_ item: Item) -> Bool {
if let newCurrentItemIndex = internalStorage.firstIndex(of: item) {
currentItemIndex = newCurrentItemIndex
return true
} else {
return false
}
}
}
//
// BackForwardListTests.swift
//
// Created by yosshi4486 on 2023/04/02.
//
import XCTest
@testable import {ProjectName}
final class BackForwardListTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testInitItems() throws {
let backForwardList = BackForwardList<String>(items: ["Item A", "Item B", "Item C"])
XCTAssertEqual(backForwardList.currentItemIndex, 2)
XCTAssertEqual(backForwardList.currentItem, "Item C")
XCTAssertTrue(backForwardList.canBack)
XCTAssertFalse(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item B")
XCTAssertEqual(backForwardList.backItems, ["Item B", "Item A"])
XCTAssertNil(backForwardList.forwardItem)
XCTAssertEqual(backForwardList.forwardItems, [])
XCTAssertEqual(backForwardList[0], "Item A")
XCTAssertEqual(backForwardList[1], "Item B")
XCTAssertEqual(backForwardList[2], "Item C")
}
func testAddItem() throws {
var backForwardList = BackForwardList<String>()
XCTAssertEqual(backForwardList.currentItemIndex, -1)
XCTAssertNil(backForwardList.currentItem)
XCTAssertFalse(backForwardList.canBack)
XCTAssertFalse(backForwardList.canForward)
XCTAssertNil(backForwardList.backItem)
XCTAssertEqual(backForwardList.backItems, [])
XCTAssertNil(backForwardList.forwardItem)
XCTAssertEqual(backForwardList.forwardItems, [])
backForwardList.add("Item A")
XCTAssertEqual(backForwardList.currentItemIndex, 0)
XCTAssertEqual(backForwardList.currentItem, "Item A")
XCTAssertFalse(backForwardList.canBack)
XCTAssertFalse(backForwardList.canForward)
XCTAssertNil(backForwardList.backItem)
XCTAssertEqual(backForwardList.backItems, [])
XCTAssertNil(backForwardList.forwardItem)
XCTAssertEqual(backForwardList.forwardItems, [])
backForwardList.add("Item B")
XCTAssertEqual(backForwardList.currentItemIndex, 1)
XCTAssertEqual(backForwardList.currentItem, "Item B")
XCTAssertTrue(backForwardList.canBack)
XCTAssertFalse(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item A")
XCTAssertEqual(backForwardList.backItems, ["Item A"])
XCTAssertNil(backForwardList.forwardItem)
XCTAssertEqual(backForwardList.forwardItems, [])
backForwardList.add("Item C")
XCTAssertEqual(backForwardList.currentItemIndex, 2)
XCTAssertEqual(backForwardList.currentItem, "Item C")
XCTAssertTrue(backForwardList.canBack)
XCTAssertFalse(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item B")
XCTAssertEqual(backForwardList.backItems, ["Item B", "Item A"])
XCTAssertNil(backForwardList.forwardItem)
XCTAssertEqual(backForwardList.forwardItems, [])
}
func testAddItemWhenAllowConsecutiveItemsTrue() {
var backForwardList = BackForwardList<String>(items: ["Item A"])
backForwardList.allowsConsecutiveItems = true
backForwardList.add("Item A")
XCTAssertEqual(backForwardList.currentItemIndex, 1)
XCTAssertEqual(backForwardList.currentItem, "Item A")
XCTAssertTrue(backForwardList.canBack)
XCTAssertFalse(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item A")
XCTAssertEqual(backForwardList.backItems, ["Item A"])
XCTAssertNil(backForwardList.forwardItem)
XCTAssertEqual(backForwardList.forwardItems, [])
XCTAssertEqual(backForwardList.count, 2)
XCTAssertEqual(backForwardList[0], "Item A")
XCTAssertEqual(backForwardList[1], "Item A")
}
func testAddItemWhenAllowConsecutiveItemsFalse() {
var backForwardList = BackForwardList<String>(items: ["Item A"])
backForwardList.allowsConsecutiveItems = false
backForwardList.add("Item A")
XCTAssertEqual(backForwardList.currentItemIndex, 0)
XCTAssertEqual(backForwardList.currentItem, "Item A")
XCTAssertFalse(backForwardList.canBack)
XCTAssertFalse(backForwardList.canForward)
XCTAssertNil(backForwardList.backItem)
XCTAssertEqual(backForwardList.backItems, [])
XCTAssertNil(backForwardList.forwardItem)
XCTAssertEqual(backForwardList.forwardItems, [])
XCTAssertEqual(backForwardList.count, 1)
XCTAssertEqual(backForwardList[0], "Item A")
}
/// - Precondition: `testInitItems`と`testBack`はパスしていることとする.
func testAddItemWhenCurrentItemIsHalfPosition() {
var backForwardList = BackForwardList<String>(items: ["Item A", "Item B", "Item C"])
backForwardList.back()
backForwardList.back()
backForwardList.add("Item D")
XCTAssertEqual(backForwardList.currentItemIndex, 1)
XCTAssertEqual(backForwardList.currentItem, "Item D")
XCTAssertTrue(backForwardList.canBack)
XCTAssertFalse(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item A")
XCTAssertEqual(backForwardList.backItems, ["Item A"])
XCTAssertNil(backForwardList.forwardItem)
XCTAssertEqual(backForwardList.forwardItems, [])
}
// curretItemIndexはprivate(set)で動かすにはback()とforward()を使うしかないのでまとめたテストになってしまう。
/// - Precondition: `testInitItems`はパスしていることとする.
func testBackAndForward() {
var backForwardList = BackForwardList<String>(items: ["Item A", "Item B", "Item C"])
backForwardList.back()
XCTAssertEqual(backForwardList.currentItemIndex, 1)
XCTAssertEqual(backForwardList.currentItem, "Item B")
XCTAssertTrue(backForwardList.canBack)
XCTAssertTrue(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item A")
XCTAssertEqual(backForwardList.backItems, ["Item A"])
XCTAssertEqual(backForwardList.forwardItem, "Item C")
XCTAssertEqual(backForwardList.forwardItems, ["Item C"])
backForwardList.back()
XCTAssertEqual(backForwardList.currentItemIndex, 0)
XCTAssertEqual(backForwardList.currentItem, "Item A")
XCTAssertFalse(backForwardList.canBack)
XCTAssertTrue(backForwardList.canForward)
XCTAssertNil(backForwardList.backItem)
XCTAssertEqual(backForwardList.backItems, [])
XCTAssertEqual(backForwardList.forwardItem, "Item B")
XCTAssertEqual(backForwardList.forwardItems, ["Item B", "Item C"])
// canBackがfalseの場合の実行はどうか。
backForwardList.back()
XCTAssertEqual(backForwardList.currentItemIndex, 0)
XCTAssertEqual(backForwardList.currentItem, "Item A")
XCTAssertFalse(backForwardList.canBack)
XCTAssertTrue(backForwardList.canForward)
XCTAssertNil(backForwardList.backItem)
XCTAssertEqual(backForwardList.backItems, [])
XCTAssertEqual(backForwardList.forwardItem, "Item B")
XCTAssertEqual(backForwardList.forwardItems, ["Item B", "Item C"])
backForwardList.forward()
XCTAssertEqual(backForwardList.currentItemIndex, 1)
XCTAssertEqual(backForwardList.currentItem, "Item B")
XCTAssertTrue(backForwardList.canBack)
XCTAssertTrue(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item A")
XCTAssertEqual(backForwardList.backItems, ["Item A"])
XCTAssertEqual(backForwardList.forwardItem, "Item C")
XCTAssertEqual(backForwardList.forwardItems, ["Item C"])
backForwardList.forward()
XCTAssertEqual(backForwardList.currentItemIndex, 2)
XCTAssertEqual(backForwardList.currentItem, "Item C")
XCTAssertTrue(backForwardList.canBack)
XCTAssertFalse(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item B")
XCTAssertEqual(backForwardList.backItems, ["Item B", "Item A"])
XCTAssertNil(backForwardList.forwardItem)
XCTAssertEqual(backForwardList.forwardItems, [])
// canForwardがfalseの場合の実行はどうか。
backForwardList.forward()
XCTAssertEqual(backForwardList.currentItemIndex, 2)
XCTAssertEqual(backForwardList.currentItem, "Item C")
XCTAssertTrue(backForwardList.canBack)
XCTAssertFalse(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item B")
XCTAssertEqual(backForwardList.backItems, ["Item B", "Item A"])
XCTAssertNil(backForwardList.forwardItem)
XCTAssertEqual(backForwardList.forwardItems, [])
}
func testMakeCurrentItem() {
var backForwardList = BackForwardList<String>(items: ["Item A", "Item B", "Item C"])
let resultA = backForwardList.makeCurrentItem("Item A")
XCTAssertTrue(resultA)
XCTAssertEqual(backForwardList.currentItemIndex, 0)
XCTAssertEqual(backForwardList.currentItem, "Item A")
XCTAssertFalse(backForwardList.canBack)
XCTAssertTrue(backForwardList.canForward)
XCTAssertNil(backForwardList.backItem)
XCTAssertEqual(backForwardList.backItems, [])
XCTAssertEqual(backForwardList.forwardItem, "Item B")
XCTAssertEqual(backForwardList.forwardItems, ["Item B", "Item C"])
let resultC = backForwardList.makeCurrentItem("Item C")
XCTAssertTrue(resultC)
XCTAssertEqual(backForwardList.currentItemIndex, 2)
XCTAssertEqual(backForwardList.currentItem, "Item C")
XCTAssertTrue(backForwardList.canBack)
XCTAssertFalse(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item B")
XCTAssertEqual(backForwardList.backItems, ["Item B", "Item A"])
XCTAssertNil(backForwardList.forwardItem)
XCTAssertEqual(backForwardList.forwardItems, [])
let resultB = backForwardList.makeCurrentItem("Item B")
XCTAssertTrue(resultB)
XCTAssertEqual(backForwardList.currentItemIndex, 1)
XCTAssertEqual(backForwardList.currentItem, "Item B")
XCTAssertTrue(backForwardList.canBack)
XCTAssertTrue(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item A")
XCTAssertEqual(backForwardList.backItems, ["Item A"])
XCTAssertEqual(backForwardList.forwardItem, "Item C")
XCTAssertEqual(backForwardList.forwardItems, ["Item C"])
let resultUnknown = backForwardList.makeCurrentItem("Item X")
XCTAssertFalse(resultUnknown)
XCTAssertEqual(backForwardList.currentItemIndex, 1)
XCTAssertEqual(backForwardList.currentItem, "Item B")
XCTAssertTrue(backForwardList.canBack)
XCTAssertTrue(backForwardList.canForward)
XCTAssertEqual(backForwardList.backItem, "Item A")
XCTAssertEqual(backForwardList.backItems, ["Item A"])
XCTAssertEqual(backForwardList.forwardItem, "Item C")
XCTAssertEqual(backForwardList.forwardItems, ["Item C"])
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment